Home | History | Annotate | Download | only in agenda
      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.agenda;
     18 
     19 import android.app.Activity;
     20 import android.content.AsyncQueryHandler;
     21 import android.content.ContentResolver;
     22 import android.content.ContentUris;
     23 import android.content.Context;
     24 import android.content.res.Resources;
     25 import android.database.Cursor;
     26 import android.net.Uri;
     27 import android.os.Handler;
     28 import android.provider.CalendarContract;
     29 import android.provider.CalendarContract.Attendees;
     30 import android.provider.CalendarContract.Calendars;
     31 import android.provider.CalendarContract.Instances;
     32 import android.text.format.DateUtils;
     33 import android.text.format.Time;
     34 import android.util.Log;
     35 import android.view.LayoutInflater;
     36 import android.view.View;
     37 import android.view.View.OnClickListener;
     38 import android.view.ViewGroup;
     39 import android.widget.AbsListView.OnScrollListener;
     40 import android.widget.BaseAdapter;
     41 import android.widget.GridLayout;
     42 import android.widget.TextView;
     43 
     44 import com.android.calendar.CalendarController;
     45 import com.android.calendar.CalendarController.EventType;
     46 import com.android.calendar.CalendarController.ViewType;
     47 import com.android.calendar.R;
     48 import com.android.calendar.StickyHeaderListView;
     49 import com.android.calendar.Utils;
     50 
     51 import java.util.Date;
     52 import java.util.Formatter;
     53 import java.util.Iterator;
     54 import java.util.LinkedList;
     55 import java.util.Locale;
     56 import java.util.concurrent.ConcurrentLinkedQueue;
     57 
     58 /*
     59 Bugs Bugs Bugs:
     60 - At rotation and launch time, the initial position is not set properly. This code is calling
     61  listview.setSelection() in 2 rapid secessions but it dropped or didn't process the first one.
     62 - Scroll using trackball isn't repositioning properly after a new adapter is added.
     63 - Track ball clicks at the header/footer doesn't work.
     64 - Potential ping pong effect if the prefetch window is big and data is limited
     65 - Add index in calendar provider
     66 
     67 ToDo ToDo ToDo:
     68 Get design of header and footer from designer
     69 
     70 Make scrolling smoother.
     71 Test for correctness
     72 Loading speed
     73 Check for leaks and excessive allocations
     74  */
     75 
     76 public class AgendaWindowAdapter extends BaseAdapter
     77     implements StickyHeaderListView.HeaderIndexer, StickyHeaderListView.HeaderHeightListener{
     78 
     79     static final boolean BASICLOG = false;
     80     static final boolean DEBUGLOG = false;
     81     private static final String TAG = "AgendaWindowAdapter";
     82 
     83     private static final String AGENDA_SORT_ORDER =
     84             CalendarContract.Instances.START_DAY + " ASC, " +
     85             CalendarContract.Instances.BEGIN + " ASC, " +
     86             CalendarContract.Events.TITLE + " ASC";
     87 
     88     public static final int INDEX_INSTANCE_ID = 0;
     89     public static final int INDEX_TITLE = 1;
     90     public static final int INDEX_EVENT_LOCATION = 2;
     91     public static final int INDEX_ALL_DAY = 3;
     92     public static final int INDEX_HAS_ALARM = 4;
     93     public static final int INDEX_COLOR = 5;
     94     public static final int INDEX_RRULE = 6;
     95     public static final int INDEX_BEGIN = 7;
     96     public static final int INDEX_END = 8;
     97     public static final int INDEX_EVENT_ID = 9;
     98     public static final int INDEX_START_DAY = 10;
     99     public static final int INDEX_END_DAY = 11;
    100     public static final int INDEX_SELF_ATTENDEE_STATUS = 12;
    101     public static final int INDEX_ORGANIZER = 13;
    102     public static final int INDEX_OWNER_ACCOUNT = 14;
    103     public static final int INDEX_CAN_ORGANIZER_RESPOND= 15;
    104     public static final int INDEX_TIME_ZONE = 16;
    105 
    106     private static final String[] PROJECTION = new String[] {
    107             Instances._ID, // 0
    108             Instances.TITLE, // 1
    109             Instances.EVENT_LOCATION, // 2
    110             Instances.ALL_DAY, // 3
    111             Instances.HAS_ALARM, // 4
    112             Instances.DISPLAY_COLOR, // 5 If SDK < 16, set to Instances.CALENDAR_COLOR.
    113             Instances.RRULE, // 6
    114             Instances.BEGIN, // 7
    115             Instances.END, // 8
    116             Instances.EVENT_ID, // 9
    117             Instances.START_DAY, // 10 Julian start day
    118             Instances.END_DAY, // 11 Julian end day
    119             Instances.SELF_ATTENDEE_STATUS, // 12
    120             Instances.ORGANIZER, // 13
    121             Instances.OWNER_ACCOUNT, // 14
    122             Instances.CAN_ORGANIZER_RESPOND, // 15
    123             Instances.EVENT_TIMEZONE, // 16
    124     };
    125 
    126     static {
    127         if (!Utils.isJellybeanOrLater()) {
    128             PROJECTION[INDEX_COLOR] = Instances.CALENDAR_COLOR;
    129         }
    130     }
    131 
    132     // Listview may have a bug where the index/position is not consistent when there's a header.
    133     // position == positionInListView - OFF_BY_ONE_BUG
    134     // TODO Need to look into this.
    135     private static final int OFF_BY_ONE_BUG = 1;
    136     private static final int MAX_NUM_OF_ADAPTERS = 5;
    137     private static final int IDEAL_NUM_OF_EVENTS = 50;
    138     private static final int MIN_QUERY_DURATION = 7; // days
    139     private static final int MAX_QUERY_DURATION = 60; // days
    140     private static final int PREFETCH_BOUNDARY = 1;
    141 
    142     /** Times to auto-expand/retry query after getting no data */
    143     private static final int RETRIES_ON_NO_DATA = 1;
    144 
    145     private final Context mContext;
    146     private final Resources mResources;
    147     private final QueryHandler mQueryHandler;
    148     private final AgendaListView mAgendaListView;
    149 
    150     /** The sum of the rows in all the adapters */
    151     private int mRowCount;
    152 
    153     /** The number of times we have queried and gotten no results back */
    154     private int mEmptyCursorCount;
    155 
    156     /** Cached value of the last used adapter */
    157     private DayAdapterInfo mLastUsedInfo;
    158 
    159     private final LinkedList<DayAdapterInfo> mAdapterInfos =
    160             new LinkedList<DayAdapterInfo>();
    161     private final ConcurrentLinkedQueue<QuerySpec> mQueryQueue =
    162             new ConcurrentLinkedQueue<QuerySpec>();
    163     private final TextView mHeaderView;
    164     private final TextView mFooterView;
    165     private boolean mDoneSettingUpHeaderFooter = false;
    166 
    167     private final boolean mIsTabletConfig;
    168 
    169     boolean mCleanQueryInitiated = false;
    170     private int mStickyHeaderSize = 44; // Initial size big enough for it to work
    171 
    172     /**
    173      * When the user scrolled to the top, a query will be made for older events
    174      * and this will be incremented. Don't make more requests if
    175      * mOlderRequests > mOlderRequestsProcessed.
    176      */
    177     private int mOlderRequests;
    178 
    179     /** Number of "older" query that has been processed. */
    180     private int mOlderRequestsProcessed;
    181 
    182     /**
    183      * When the user scrolled to the bottom, a query will be made for newer
    184      * events and this will be incremented. Don't make more requests if
    185      * mNewerRequests > mNewerRequestsProcessed.
    186      */
    187     private int mNewerRequests;
    188 
    189     /** Number of "newer" query that has been processed. */
    190     private int mNewerRequestsProcessed;
    191 
    192     // Note: Formatter is not thread safe. Fine for now as it is only used by the main thread.
    193     private final Formatter mFormatter;
    194     private final StringBuilder mStringBuilder;
    195     private String mTimeZone;
    196 
    197     // defines if to pop-up the current event when the agenda is first shown
    198     private final boolean mShowEventOnStart;
    199 
    200     private final Runnable mTZUpdater = new Runnable() {
    201         @Override
    202         public void run() {
    203             mTimeZone = Utils.getTimeZone(mContext, this);
    204             notifyDataSetChanged();
    205         }
    206     };
    207 
    208     private final Handler mDataChangedHandler = new Handler();
    209     private final Runnable mDataChangedRunnable = new Runnable() {
    210         @Override
    211         public void run() {
    212             notifyDataSetChanged();
    213         }
    214     };
    215 
    216     private boolean mShuttingDown;
    217     private boolean mHideDeclined;
    218 
    219     // Used to stop a fling motion if the ListView is set to a specific position
    220     int mListViewScrollState = OnScrollListener.SCROLL_STATE_IDLE;
    221 
    222     /** The current search query, or null if none */
    223     private String mSearchQuery;
    224 
    225     private long mSelectedInstanceId = -1;
    226 
    227     private final int mSelectedItemBackgroundColor;
    228     private final int mSelectedItemTextColor;
    229     private final float mItemRightMargin;
    230 
    231     // Types of Query
    232     private static final int QUERY_TYPE_OLDER = 0; // Query for older events
    233     private static final int QUERY_TYPE_NEWER = 1; // Query for newer events
    234     private static final int QUERY_TYPE_CLEAN = 2; // Delete everything and query around a date
    235 
    236     private static class QuerySpec {
    237         long queryStartMillis;
    238         Time goToTime;
    239         int start;
    240         int end;
    241         String searchQuery;
    242         int queryType;
    243         long id;
    244 
    245         public QuerySpec(int queryType) {
    246             this.queryType = queryType;
    247             id = -1;
    248         }
    249 
    250         @Override
    251         public int hashCode() {
    252             final int prime = 31;
    253             int result = 1;
    254             result = prime * result + end;
    255             result = prime * result + (int) (queryStartMillis ^ (queryStartMillis >>> 32));
    256             result = prime * result + queryType;
    257             result = prime * result + start;
    258             if (searchQuery != null) {
    259                 result = prime * result + searchQuery.hashCode();
    260             }
    261             if (goToTime != null) {
    262                 long goToTimeMillis = goToTime.toMillis(false);
    263                 result = prime * result + (int) (goToTimeMillis ^ (goToTimeMillis >>> 32));
    264             }
    265             result = prime * result + (int)id;
    266             return result;
    267         }
    268 
    269         @Override
    270         public boolean equals(Object obj) {
    271             if (this == obj) return true;
    272             if (obj == null) return false;
    273             if (getClass() != obj.getClass()) return false;
    274             QuerySpec other = (QuerySpec) obj;
    275             if (end != other.end || queryStartMillis != other.queryStartMillis
    276                     || queryType != other.queryType || start != other.start
    277                     || Utils.equals(searchQuery, other.searchQuery) || id != other.id) {
    278                 return false;
    279             }
    280 
    281             if (goToTime != null) {
    282                 if (goToTime.toMillis(false) != other.goToTime.toMillis(false)) {
    283                     return false;
    284                 }
    285             } else {
    286                 if (other.goToTime != null) {
    287                     return false;
    288                 }
    289             }
    290             return true;
    291         }
    292     }
    293 
    294     /**
    295      * Class representing a list item within the Agenda view.  Could be either an instance of an
    296      * event, or a header marking the specific day.
    297      *
    298      * The begin and end times of an AgendaItem should always be in local time, even if the event
    299      * is all day.  buildAgendaItemFromCursor() converts each event to local time.
    300      */
    301     static class AgendaItem {
    302         long begin;
    303         long end;
    304         long id;
    305         int startDay;
    306         boolean allDay;
    307     }
    308 
    309     static class DayAdapterInfo {
    310         Cursor cursor;
    311         AgendaByDayAdapter dayAdapter;
    312         int start; // start day of the cursor's coverage
    313         int end; // end day of the cursor's coverage
    314         int offset; // offset in position in the list view
    315         int size; // dayAdapter.getCount()
    316 
    317         public DayAdapterInfo(Context context) {
    318             dayAdapter = new AgendaByDayAdapter(context);
    319         }
    320 
    321         @Override
    322         public String toString() {
    323             // Static class, so the time in this toString will not reflect the
    324             // home tz settings. This should only affect debugging.
    325             Time time = new Time();
    326             StringBuilder sb = new StringBuilder();
    327             time.setJulianDay(start);
    328             time.normalize(false);
    329             sb.append("Start:").append(time.toString());
    330             time.setJulianDay(end);
    331             time.normalize(false);
    332             sb.append(" End:").append(time.toString());
    333             sb.append(" Offset:").append(offset);
    334             sb.append(" Size:").append(size);
    335             return sb.toString();
    336         }
    337     }
    338 
    339     public AgendaWindowAdapter(Context context,
    340             AgendaListView agendaListView, boolean showEventOnStart) {
    341         mContext = context;
    342         mResources = context.getResources();
    343         mSelectedItemBackgroundColor = mResources
    344                 .getColor(R.color.agenda_selected_background_color);
    345         mSelectedItemTextColor = mResources.getColor(R.color.agenda_selected_text_color);
    346         mItemRightMargin = mResources.getDimension(R.dimen.agenda_item_right_margin);
    347         mIsTabletConfig = Utils.getConfigBool(mContext, R.bool.tablet_config);
    348 
    349         mTimeZone = Utils.getTimeZone(context, mTZUpdater);
    350         mAgendaListView = agendaListView;
    351         mQueryHandler = new QueryHandler(context.getContentResolver());
    352 
    353         mStringBuilder = new StringBuilder(50);
    354         mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
    355 
    356         mShowEventOnStart = showEventOnStart;
    357 
    358         // Implies there is no sticky header
    359         if (!mShowEventOnStart) {
    360             mStickyHeaderSize = 0;
    361         }
    362         mSearchQuery = null;
    363 
    364         LayoutInflater inflater = (LayoutInflater) context
    365                 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    366         mHeaderView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
    367         mFooterView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
    368         mHeaderView.setText(R.string.loading);
    369         mAgendaListView.addHeaderView(mHeaderView);
    370     }
    371 
    372     // Method in Adapter
    373     @Override
    374     public int getViewTypeCount() {
    375         return AgendaByDayAdapter.TYPE_LAST;
    376     }
    377 
    378     // Method in BaseAdapter
    379     @Override
    380     public boolean areAllItemsEnabled() {
    381         return false;
    382     }
    383 
    384     // Method in Adapter
    385     @Override
    386     public int getItemViewType(int position) {
    387         DayAdapterInfo info = getAdapterInfoByPosition(position);
    388         if (info != null) {
    389             return info.dayAdapter.getItemViewType(position - info.offset);
    390         } else {
    391             return -1;
    392         }
    393     }
    394 
    395     // Method in BaseAdapter
    396     @Override
    397     public boolean isEnabled(int position) {
    398         DayAdapterInfo info = getAdapterInfoByPosition(position);
    399         if (info != null) {
    400             return info.dayAdapter.isEnabled(position - info.offset);
    401         } else {
    402             return false;
    403         }
    404     }
    405 
    406     // Abstract Method in BaseAdapter
    407     public int getCount() {
    408         return mRowCount;
    409     }
    410 
    411     // Abstract Method in BaseAdapter
    412     public Object getItem(int position) {
    413         DayAdapterInfo info = getAdapterInfoByPosition(position);
    414         if (info != null) {
    415             return info.dayAdapter.getItem(position - info.offset);
    416         } else {
    417             return null;
    418         }
    419     }
    420 
    421     // Method in BaseAdapter
    422     @Override
    423     public boolean hasStableIds() {
    424         return true;
    425     }
    426 
    427     // Abstract Method in BaseAdapter
    428     @Override
    429     public long getItemId(int position) {
    430         DayAdapterInfo info = getAdapterInfoByPosition(position);
    431         if (info != null) {
    432             int curPos = info.dayAdapter.getCursorPosition(position - info.offset);
    433             if (curPos == Integer.MIN_VALUE) {
    434                 return -1;
    435             }
    436             // Regular event
    437             if (curPos >= 0) {
    438                 info.cursor.moveToPosition(curPos);
    439                 return info.cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID) << 20 +
    440                     info.cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN);
    441             }
    442             // Day Header
    443             return info.dayAdapter.findJulianDayFromPosition(position);
    444 
    445         } else {
    446             return -1;
    447         }
    448     }
    449 
    450     // Abstract Method in BaseAdapter
    451     public View getView(int position, View convertView, ViewGroup parent) {
    452         if (position >= (mRowCount - PREFETCH_BOUNDARY)
    453                 && mNewerRequests <= mNewerRequestsProcessed) {
    454             if (DEBUGLOG) Log.e(TAG, "queryForNewerEvents: ");
    455             mNewerRequests++;
    456             queueQuery(new QuerySpec(QUERY_TYPE_NEWER));
    457         }
    458 
    459         if (position < PREFETCH_BOUNDARY
    460                 && mOlderRequests <= mOlderRequestsProcessed) {
    461             if (DEBUGLOG) Log.e(TAG, "queryForOlderEvents: ");
    462             mOlderRequests++;
    463             queueQuery(new QuerySpec(QUERY_TYPE_OLDER));
    464         }
    465 
    466         final View v;
    467         DayAdapterInfo info = getAdapterInfoByPosition(position);
    468         if (info != null) {
    469             int offset = position - info.offset;
    470             v = info.dayAdapter.getView(offset, convertView,
    471                     parent);
    472 
    473             // Turn on the past/present separator if the view is a day header
    474             // and it is the first day with events after yesterday.
    475             if (info.dayAdapter.isDayHeaderView(offset)) {
    476                 View simpleDivider = v.findViewById(R.id.top_divider_simple);
    477                 View pastPresentDivider = v.findViewById(R.id.top_divider_past_present);
    478                 if (info.dayAdapter.isFirstDayAfterYesterday(offset)) {
    479                     if (simpleDivider != null && pastPresentDivider != null) {
    480                         simpleDivider.setVisibility(View.GONE);
    481                         pastPresentDivider.setVisibility(View.VISIBLE);
    482                     }
    483                 } else if (simpleDivider != null && pastPresentDivider != null) {
    484                     simpleDivider.setVisibility(View.VISIBLE);
    485                     pastPresentDivider.setVisibility(View.GONE);
    486                 }
    487             }
    488         } else {
    489             // TODO
    490             Log.e(TAG, "BUG: getAdapterInfoByPosition returned null!!! " + position);
    491             TextView tv = new TextView(mContext);
    492             tv.setText("Bug! " + position);
    493             v = tv;
    494         }
    495 
    496         // If this is not a tablet config don't do selection highlighting
    497         if (!mIsTabletConfig) {
    498             return v;
    499         }
    500         // Show selected marker if this is item is selected
    501         boolean selected = false;
    502         Object yy = v.getTag();
    503         if (yy instanceof AgendaAdapter.ViewHolder) {
    504             AgendaAdapter.ViewHolder vh = (AgendaAdapter.ViewHolder) yy;
    505             selected = mSelectedInstanceId == vh.instanceId;
    506             vh.selectedMarker.setVisibility((selected && mShowEventOnStart) ?
    507                     View.VISIBLE : View.GONE);
    508             if (mShowEventOnStart) {
    509                 GridLayout.LayoutParams lp =
    510                         (GridLayout.LayoutParams)vh.textContainer.getLayoutParams();
    511                 if (selected) {
    512                     mSelectedVH = vh;
    513                     v.setBackgroundColor(mSelectedItemBackgroundColor);
    514                     vh.title.setTextColor(mSelectedItemTextColor);
    515                     vh.when.setTextColor(mSelectedItemTextColor);
    516                     vh.where.setTextColor(mSelectedItemTextColor);
    517                     lp.setMargins(0, 0, 0, 0);
    518                     vh.textContainer.setLayoutParams(lp);
    519                 } else {
    520                     lp.setMargins(0, 0, (int)mItemRightMargin, 0);
    521                     vh.textContainer.setLayoutParams(lp);
    522                 }
    523             }
    524         }
    525 
    526         if (DEBUGLOG) {
    527             Log.e(TAG, "getView " + position + " = " + getViewTitle(v));
    528         }
    529         return v;
    530     }
    531 
    532     private AgendaAdapter.ViewHolder mSelectedVH = null;
    533 
    534     private int findEventPositionNearestTime(Time time, long id) {
    535         DayAdapterInfo info = getAdapterInfoByTime(time);
    536         int pos = -1;
    537         if (info != null) {
    538             pos = info.offset + info.dayAdapter.findEventPositionNearestTime(time, id);
    539         }
    540         if (DEBUGLOG) Log.e(TAG, "findEventPositionNearestTime " + time + " id:" + id + " =" + pos);
    541         return pos;
    542     }
    543 
    544     protected DayAdapterInfo getAdapterInfoByPosition(int position) {
    545         synchronized (mAdapterInfos) {
    546             if (mLastUsedInfo != null && mLastUsedInfo.offset <= position
    547                     && position < (mLastUsedInfo.offset + mLastUsedInfo.size)) {
    548                 return mLastUsedInfo;
    549             }
    550             for (DayAdapterInfo info : mAdapterInfos) {
    551                 if (info.offset <= position
    552                         && position < (info.offset + info.size)) {
    553                     mLastUsedInfo = info;
    554                     return info;
    555                 }
    556             }
    557         }
    558         return null;
    559     }
    560 
    561     private DayAdapterInfo getAdapterInfoByTime(Time time) {
    562         if (DEBUGLOG) Log.e(TAG, "getAdapterInfoByTime " + time.toString());
    563 
    564         Time tmpTime = new Time(time);
    565         long timeInMillis = tmpTime.normalize(true);
    566         int day = Time.getJulianDay(timeInMillis, tmpTime.gmtoff);
    567         synchronized (mAdapterInfos) {
    568             for (DayAdapterInfo info : mAdapterInfos) {
    569                 if (info.start <= day && day <= info.end) {
    570                     return info;
    571                 }
    572             }
    573         }
    574         return null;
    575     }
    576 
    577     public AgendaItem getAgendaItemByPosition(final int positionInListView) {
    578         return getAgendaItemByPosition(positionInListView, true);
    579     }
    580 
    581     /**
    582      * Return the event info for a given position in the adapter
    583      * @param positionInListView
    584      * @param returnEventStartDay If true, return actual event startday. Otherwise
    585      *        return agenda date-header date as the startDay.
    586      *        The two will differ for multi-day events after the first day.
    587      * @return
    588      */
    589     public AgendaItem getAgendaItemByPosition(final int positionInListView,
    590             boolean returnEventStartDay) {
    591         if (DEBUGLOG) Log.e(TAG, "getEventByPosition " + positionInListView);
    592         if (positionInListView < 0) {
    593             return null;
    594         }
    595 
    596         final int positionInAdapter = positionInListView - OFF_BY_ONE_BUG;
    597         DayAdapterInfo info = getAdapterInfoByPosition(positionInAdapter);
    598         if (info == null) {
    599             return null;
    600         }
    601 
    602         int cursorPosition = info.dayAdapter.getCursorPosition(positionInAdapter - info.offset);
    603         if (cursorPosition == Integer.MIN_VALUE) {
    604             return null;
    605         }
    606 
    607         boolean isDayHeader = false;
    608         if (cursorPosition < 0) {
    609             cursorPosition = -cursorPosition;
    610             isDayHeader = true;
    611         }
    612 
    613         if (cursorPosition < info.cursor.getCount()) {
    614             AgendaItem item = buildAgendaItemFromCursor(info.cursor, cursorPosition, isDayHeader);
    615             if (!returnEventStartDay && !isDayHeader) {
    616                 item.startDay = info.dayAdapter.findJulianDayFromPosition(positionInAdapter -
    617                         info.offset);
    618             }
    619             return item;
    620         }
    621         return null;
    622     }
    623 
    624     private AgendaItem buildAgendaItemFromCursor(final Cursor cursor, int cursorPosition,
    625             boolean isDayHeader) {
    626         if (cursorPosition == -1) {
    627             cursor.moveToFirst();
    628         } else {
    629             cursor.moveToPosition(cursorPosition);
    630         }
    631         AgendaItem agendaItem = new AgendaItem();
    632         agendaItem.begin = cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN);
    633         agendaItem.end = cursor.getLong(AgendaWindowAdapter.INDEX_END);
    634         agendaItem.startDay = cursor.getInt(AgendaWindowAdapter.INDEX_START_DAY);
    635         agendaItem.allDay = cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0;
    636         if (agendaItem.allDay) { // UTC to Local time conversion
    637             Time time = new Time(mTimeZone);
    638             time.setJulianDay(Time.getJulianDay(agendaItem.begin, 0));
    639             agendaItem.begin = time.toMillis(false /* use isDst */);
    640         } else if (isDayHeader) { // Trim to midnight.
    641             Time time = new Time(mTimeZone);
    642             time.set(agendaItem.begin);
    643             time.hour = 0;
    644             time.minute = 0;
    645             time.second = 0;
    646             agendaItem.begin = time.toMillis(false /* use isDst */);
    647         }
    648 
    649         // If this is not a day header, then it's an event.
    650         if (!isDayHeader) {
    651             agendaItem.id = cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID);
    652             if (agendaItem.allDay) {
    653                 Time time = new Time(mTimeZone);
    654                 time.setJulianDay(Time.getJulianDay(agendaItem.end, 0));
    655                 agendaItem.end = time.toMillis(false /* use isDst */);
    656             }
    657         }
    658         return agendaItem;
    659     }
    660 
    661     /**
    662      * Ensures that any all day events are converted to UTC before a VIEW_EVENT command is sent.
    663      */
    664     private void sendViewEvent(AgendaItem item, long selectedTime) {
    665         long startTime;
    666         long endTime;
    667         if (item.allDay) {
    668             startTime = Utils.convertAlldayLocalToUTC(null, item.begin, mTimeZone);
    669             endTime = Utils.convertAlldayLocalToUTC(null, item.end, mTimeZone);
    670         } else {
    671             startTime = item.begin;
    672             endTime = item.end;
    673         }
    674         if (DEBUGLOG) {
    675             Log.d(TAG, "Sent (AgendaWindowAdapter): VIEW EVENT: " + new Date(startTime));
    676         }
    677         CalendarController.getInstance(mContext)
    678         .sendEventRelatedEventWithExtra(this, EventType.VIEW_EVENT,
    679                 item.id, startTime, endTime, 0,
    680                 0, CalendarController.EventInfo.buildViewExtraLong(
    681                         Attendees.ATTENDEE_STATUS_NONE,
    682                         item.allDay), selectedTime);
    683     }
    684 
    685     public void refresh(Time goToTime, long id, String searchQuery, boolean forced,
    686             boolean refreshEventInfo) {
    687         if (searchQuery != null) {
    688             mSearchQuery = searchQuery;
    689         }
    690 
    691         if (DEBUGLOG) {
    692             Log.e(TAG, this + ": refresh " + goToTime.toString() + " id " + id
    693                     + ((searchQuery != null) ? searchQuery : "")
    694                     + (forced ? " forced" : " not forced")
    695                     + (refreshEventInfo ? " refresh event info" : ""));
    696         }
    697 
    698         int startDay = Time.getJulianDay(goToTime.toMillis(false), goToTime.gmtoff);
    699 
    700         if (!forced && isInRange(startDay, startDay)) {
    701             // No need to re-query
    702             if (!mAgendaListView.isAgendaItemVisible(goToTime, id)) {
    703                 int gotoPosition = findEventPositionNearestTime(goToTime, id);
    704                 if (gotoPosition > 0) {
    705                     mAgendaListView.setSelectionFromTop(gotoPosition +
    706                             OFF_BY_ONE_BUG, mStickyHeaderSize);
    707                     if (mListViewScrollState == OnScrollListener.SCROLL_STATE_FLING) {
    708                         mAgendaListView.smoothScrollBy(0, 0);
    709                     }
    710                     if (refreshEventInfo) {
    711                         long newInstanceId = findInstanceIdFromPosition(gotoPosition);
    712                         if (newInstanceId != getSelectedInstanceId()) {
    713                             setSelectedInstanceId(newInstanceId);
    714                             mDataChangedHandler.post(mDataChangedRunnable);
    715                             Cursor tempCursor = getCursorByPosition(gotoPosition);
    716                             if (tempCursor != null) {
    717                                 int tempCursorPosition = getCursorPositionByPosition(gotoPosition);
    718                                 AgendaItem item =
    719                                         buildAgendaItemFromCursor(tempCursor, tempCursorPosition,
    720                                                 false);
    721                                 mSelectedVH = new AgendaAdapter.ViewHolder();
    722                                 mSelectedVH.allDay = item.allDay;
    723                                 sendViewEvent(item, goToTime.toMillis(false));
    724                             }
    725                         }
    726                     }
    727                 }
    728 
    729                 Time actualTime = new Time(mTimeZone);
    730                 actualTime.set(goToTime);
    731                 CalendarController.getInstance(mContext).sendEvent(this, EventType.UPDATE_TITLE,
    732                         actualTime, actualTime, -1, ViewType.CURRENT);
    733             }
    734             return;
    735         }
    736 
    737         // If AllInOneActivity is sending a second GOTO event(in OnResume), ignore it.
    738         if (!mCleanQueryInitiated || searchQuery != null) {
    739             // Query for a total of MIN_QUERY_DURATION days
    740             int endDay = startDay + MIN_QUERY_DURATION;
    741 
    742             mSelectedInstanceId = -1;
    743             mCleanQueryInitiated = true;
    744             queueQuery(startDay, endDay, goToTime, searchQuery, QUERY_TYPE_CLEAN, id);
    745 
    746             // Pre-fetch more data to overcome a race condition in AgendaListView.shiftSelection
    747             // Queuing more data with the goToTime set to the selected time skips the call to
    748             // shiftSelection on refresh.
    749             mOlderRequests++;
    750             queueQuery(0, 0, goToTime, searchQuery, QUERY_TYPE_OLDER, id);
    751             mNewerRequests++;
    752             queueQuery(0, 0, goToTime, searchQuery, QUERY_TYPE_NEWER, id);
    753         }
    754     }
    755 
    756     public void close() {
    757         mShuttingDown = true;
    758         pruneAdapterInfo(QUERY_TYPE_CLEAN);
    759         if (mQueryHandler != null) {
    760             mQueryHandler.cancelOperation(0);
    761         }
    762     }
    763 
    764     private DayAdapterInfo pruneAdapterInfo(int queryType) {
    765         synchronized (mAdapterInfos) {
    766             DayAdapterInfo recycleMe = null;
    767             if (!mAdapterInfos.isEmpty()) {
    768                 if (mAdapterInfos.size() >= MAX_NUM_OF_ADAPTERS) {
    769                     if (queryType == QUERY_TYPE_NEWER) {
    770                         recycleMe = mAdapterInfos.removeFirst();
    771                     } else if (queryType == QUERY_TYPE_OLDER) {
    772                         recycleMe = mAdapterInfos.removeLast();
    773                         // Keep the size only if the oldest items are removed.
    774                         recycleMe.size = 0;
    775                     }
    776                     if (recycleMe != null) {
    777                         if (recycleMe.cursor != null) {
    778                             recycleMe.cursor.close();
    779                         }
    780                         return recycleMe;
    781                     }
    782                 }
    783 
    784                 if (mRowCount == 0 || queryType == QUERY_TYPE_CLEAN) {
    785                     mRowCount = 0;
    786                     int deletedRows = 0;
    787                     DayAdapterInfo info;
    788                     do {
    789                         info = mAdapterInfos.poll();
    790                         if (info != null) {
    791                             // TODO the following causes ANR's. Do this in a thread.
    792                             info.cursor.close();
    793                             deletedRows += info.size;
    794                             recycleMe = info;
    795                         }
    796                     } while (info != null);
    797 
    798                     if (recycleMe != null) {
    799                         recycleMe.cursor = null;
    800                         recycleMe.size = deletedRows;
    801                     }
    802                 }
    803             }
    804             return recycleMe;
    805         }
    806     }
    807 
    808     private String buildQuerySelection() {
    809         // Respect the preference to show/hide declined events
    810 
    811         if (mHideDeclined) {
    812             return Calendars.VISIBLE + "=1 AND "
    813                     + Instances.SELF_ATTENDEE_STATUS + "!="
    814                     + Attendees.ATTENDEE_STATUS_DECLINED;
    815         } else {
    816             return Calendars.VISIBLE + "=1";
    817         }
    818     }
    819 
    820     private Uri buildQueryUri(int start, int end, String searchQuery) {
    821         Uri rootUri = searchQuery == null ?
    822                 Instances.CONTENT_BY_DAY_URI :
    823                 Instances.CONTENT_SEARCH_BY_DAY_URI;
    824         Uri.Builder builder = rootUri.buildUpon();
    825         ContentUris.appendId(builder, start);
    826         ContentUris.appendId(builder, end);
    827         if (searchQuery != null) {
    828             builder.appendPath(searchQuery);
    829         }
    830         return builder.build();
    831     }
    832 
    833     private boolean isInRange(int start, int end) {
    834         synchronized (mAdapterInfos) {
    835             if (mAdapterInfos.isEmpty()) {
    836                 return false;
    837             }
    838             return mAdapterInfos.getFirst().start <= start && end <= mAdapterInfos.getLast().end;
    839         }
    840     }
    841 
    842     private int calculateQueryDuration(int start, int end) {
    843         int queryDuration = MAX_QUERY_DURATION;
    844         if (mRowCount != 0) {
    845             queryDuration = IDEAL_NUM_OF_EVENTS * (end - start + 1) / mRowCount;
    846         }
    847 
    848         if (queryDuration > MAX_QUERY_DURATION) {
    849             queryDuration = MAX_QUERY_DURATION;
    850         } else if (queryDuration < MIN_QUERY_DURATION) {
    851             queryDuration = MIN_QUERY_DURATION;
    852         }
    853 
    854         return queryDuration;
    855     }
    856 
    857     private boolean queueQuery(int start, int end, Time goToTime,
    858             String searchQuery, int queryType, long id) {
    859         QuerySpec queryData = new QuerySpec(queryType);
    860         queryData.goToTime = new Time(goToTime);    // Creates a new time reference per QuerySpec.
    861         queryData.start = start;
    862         queryData.end = end;
    863         queryData.searchQuery = searchQuery;
    864         queryData.id = id;
    865         return queueQuery(queryData);
    866     }
    867 
    868     private boolean queueQuery(QuerySpec queryData) {
    869         queryData.searchQuery = mSearchQuery;
    870         Boolean queuedQuery;
    871         synchronized (mQueryQueue) {
    872             queuedQuery = false;
    873             Boolean doQueryNow = mQueryQueue.isEmpty();
    874             mQueryQueue.add(queryData);
    875             queuedQuery = true;
    876             if (doQueryNow) {
    877                 doQuery(queryData);
    878             }
    879         }
    880         return queuedQuery;
    881     }
    882 
    883     private void doQuery(QuerySpec queryData) {
    884         if (!mAdapterInfos.isEmpty()) {
    885             int start = mAdapterInfos.getFirst().start;
    886             int end = mAdapterInfos.getLast().end;
    887             int queryDuration = calculateQueryDuration(start, end);
    888             switch(queryData.queryType) {
    889                 case QUERY_TYPE_OLDER:
    890                     queryData.end = start - 1;
    891                     queryData.start = queryData.end - queryDuration;
    892                     break;
    893                 case QUERY_TYPE_NEWER:
    894                     queryData.start = end + 1;
    895                     queryData.end = queryData.start + queryDuration;
    896                     break;
    897             }
    898 
    899             // By "compacting" cursors, this fixes the disco/ping-pong problem
    900             // b/5311977
    901             if (mRowCount < 20 && queryData.queryType != QUERY_TYPE_CLEAN) {
    902                 if (DEBUGLOG) {
    903                     Log.e(TAG, "Compacting cursor: mRowCount=" + mRowCount
    904                             + " totalStart:" + start
    905                             + " totalEnd:" + end
    906                             + " query.start:" + queryData.start
    907                             + " query.end:" + queryData.end);
    908                 }
    909 
    910                 queryData.queryType = QUERY_TYPE_CLEAN;
    911 
    912                 if (queryData.start > start) {
    913                     queryData.start = start;
    914                 }
    915                 if (queryData.end < end) {
    916                     queryData.end = end;
    917                 }
    918             }
    919         }
    920 
    921         if (BASICLOG) {
    922             Time time = new Time(mTimeZone);
    923             time.setJulianDay(queryData.start);
    924             Time time2 = new Time(mTimeZone);
    925             time2.setJulianDay(queryData.end);
    926             Log.v(TAG, "startQuery: " + time.toString() + " to "
    927                     + time2.toString() + " then go to " + queryData.goToTime);
    928         }
    929 
    930         mQueryHandler.cancelOperation(0);
    931         if (BASICLOG) queryData.queryStartMillis = System.nanoTime();
    932 
    933         Uri queryUri = buildQueryUri(
    934                 queryData.start, queryData.end, queryData.searchQuery);
    935         mQueryHandler.startQuery(0, queryData, queryUri,
    936                 PROJECTION, buildQuerySelection(), null,
    937                 AGENDA_SORT_ORDER);
    938     }
    939 
    940     private String formatDateString(int julianDay) {
    941         Time time = new Time(mTimeZone);
    942         time.setJulianDay(julianDay);
    943         long millis = time.toMillis(false);
    944         mStringBuilder.setLength(0);
    945         return DateUtils.formatDateRange(mContext, mFormatter, millis, millis,
    946                 DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE
    947                         | DateUtils.FORMAT_ABBREV_MONTH, mTimeZone).toString();
    948     }
    949 
    950     private void updateHeaderFooter(final int start, final int end) {
    951         mHeaderView.setText(mContext.getString(R.string.show_older_events,
    952                 formatDateString(start)));
    953         mFooterView.setText(mContext.getString(R.string.show_newer_events,
    954                 formatDateString(end)));
    955     }
    956 
    957     private class QueryHandler extends AsyncQueryHandler {
    958 
    959         public QueryHandler(ContentResolver cr) {
    960             super(cr);
    961         }
    962 
    963         @Override
    964         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
    965             if (DEBUGLOG) {
    966                 Log.d(TAG, "(+)onQueryComplete");
    967             }
    968             QuerySpec data = (QuerySpec)cookie;
    969 
    970             if (cursor == null) {
    971               if (mAgendaListView != null && mAgendaListView.getContext() instanceof Activity) {
    972                 ((Activity) mAgendaListView.getContext()).finish();
    973               }
    974               return;
    975             }
    976 
    977             if (BASICLOG) {
    978                 long queryEndMillis = System.nanoTime();
    979                 Log.e(TAG, "Query time(ms): "
    980                         + (queryEndMillis - data.queryStartMillis) / 1000000
    981                         + " Count: " + cursor.getCount());
    982             }
    983 
    984             if (data.queryType == QUERY_TYPE_CLEAN) {
    985                 mCleanQueryInitiated = false;
    986             }
    987 
    988             if (mShuttingDown) {
    989                 cursor.close();
    990                 return;
    991             }
    992 
    993             // Notify Listview of changes and update position
    994             int cursorSize = cursor.getCount();
    995             if (cursorSize > 0 || mAdapterInfos.isEmpty() || data.queryType == QUERY_TYPE_CLEAN) {
    996                 final int listPositionOffset = processNewCursor(data, cursor);
    997                 int newPosition = -1;
    998                 if (data.goToTime == null) { // Typical Scrolling type query
    999                     notifyDataSetChanged();
   1000                     if (listPositionOffset != 0) {
   1001                         mAgendaListView.shiftSelection(listPositionOffset);
   1002                     }
   1003                 } else { // refresh() called. Go to the designated position
   1004                     final Time goToTime = data.goToTime;
   1005                     notifyDataSetChanged();
   1006                     newPosition = findEventPositionNearestTime(goToTime, data.id);
   1007                     if (newPosition >= 0) {
   1008                         if (mListViewScrollState == OnScrollListener.SCROLL_STATE_FLING) {
   1009                             mAgendaListView.smoothScrollBy(0, 0);
   1010                         }
   1011                         mAgendaListView.setSelectionFromTop(newPosition + OFF_BY_ONE_BUG,
   1012                                 mStickyHeaderSize);
   1013                         Time actualTime = new Time(mTimeZone);
   1014                         actualTime.set(goToTime);
   1015                         if (DEBUGLOG) {
   1016                             Log.d(TAG, "onQueryComplete: Updating title...");
   1017                         }
   1018                         CalendarController.getInstance(mContext).sendEvent(this,
   1019                                 EventType.UPDATE_TITLE, actualTime, actualTime, -1,
   1020                                 ViewType.CURRENT);
   1021                     }
   1022                     if (DEBUGLOG) {
   1023                         Log.e(TAG, "Setting listview to " +
   1024                                 "findEventPositionNearestTime: " + (newPosition + OFF_BY_ONE_BUG));
   1025                     }
   1026                 }
   1027 
   1028                 // Make sure we change the selected instance Id only on a clean query and we
   1029                 // do not have one set already
   1030                 if (mSelectedInstanceId == -1 && newPosition != -1 &&
   1031                         data.queryType == QUERY_TYPE_CLEAN) {
   1032                     if (data.id != -1 || data.goToTime != null) {
   1033                         mSelectedInstanceId = findInstanceIdFromPosition(newPosition);
   1034                     }
   1035                 }
   1036 
   1037                 // size == 1 means a fresh query. Possibly after the data changed.
   1038                 // Let's check whether mSelectedInstanceId is still valid.
   1039                 if (mAdapterInfos.size() == 1 && mSelectedInstanceId != -1) {
   1040                     boolean found = false;
   1041                     cursor.moveToPosition(-1);
   1042                     while (cursor.moveToNext()) {
   1043                         if (mSelectedInstanceId == cursor
   1044                                 .getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID)) {
   1045                             found = true;
   1046                             break;
   1047                         }
   1048                     };
   1049 
   1050                     if (!found) {
   1051                         mSelectedInstanceId = -1;
   1052                     }
   1053                 }
   1054 
   1055                 // Show the requested event
   1056                 if (mShowEventOnStart && data.queryType == QUERY_TYPE_CLEAN) {
   1057                     Cursor tempCursor = null;
   1058                     int tempCursorPosition = -1;
   1059 
   1060                     // If no valid event is selected , just pick the first one
   1061                     if (mSelectedInstanceId == -1) {
   1062                         if (cursor.moveToFirst()) {
   1063                             mSelectedInstanceId = cursor
   1064                                     .getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID);
   1065                             // Set up a dummy view holder so we have the right all day
   1066                             // info when the view is created.
   1067                             // TODO determine the full set of what might be useful to
   1068                             // know about the selected view and fill it in.
   1069                             mSelectedVH = new AgendaAdapter.ViewHolder();
   1070                             mSelectedVH.allDay =
   1071                                 cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0;
   1072                             tempCursor = cursor;
   1073                         }
   1074                     } else if (newPosition != -1) {
   1075                          tempCursor = getCursorByPosition(newPosition);
   1076                          tempCursorPosition = getCursorPositionByPosition(newPosition);
   1077                     }
   1078                     if (tempCursor != null) {
   1079                         AgendaItem item = buildAgendaItemFromCursor(tempCursor, tempCursorPosition,
   1080                                 false);
   1081                         long selectedTime = findStartTimeFromPosition(newPosition);
   1082                         if (DEBUGLOG) {
   1083                             Log.d(TAG, "onQueryComplete: Sending View Event...");
   1084                         }
   1085                         sendViewEvent(item, selectedTime);
   1086                     }
   1087                 }
   1088             } else {
   1089                 cursor.close();
   1090             }
   1091 
   1092             // Update header and footer
   1093             if (!mDoneSettingUpHeaderFooter) {
   1094                 OnClickListener headerFooterOnClickListener = new OnClickListener() {
   1095                     public void onClick(View v) {
   1096                         if (v == mHeaderView) {
   1097                             queueQuery(new QuerySpec(QUERY_TYPE_OLDER));
   1098                         } else {
   1099                             queueQuery(new QuerySpec(QUERY_TYPE_NEWER));
   1100                         }
   1101                     }};
   1102                 mHeaderView.setOnClickListener(headerFooterOnClickListener);
   1103                 mFooterView.setOnClickListener(headerFooterOnClickListener);
   1104                 mAgendaListView.addFooterView(mFooterView);
   1105                 mDoneSettingUpHeaderFooter = true;
   1106             }
   1107             synchronized (mQueryQueue) {
   1108                 int totalAgendaRangeStart = -1;
   1109                 int totalAgendaRangeEnd = -1;
   1110 
   1111                 if (cursorSize != 0) {
   1112                     // Remove the query that just completed
   1113                     QuerySpec x = mQueryQueue.poll();
   1114                     if (BASICLOG && !x.equals(data)) {
   1115                         Log.e(TAG, "onQueryComplete - cookie != head of queue");
   1116                     }
   1117                     mEmptyCursorCount = 0;
   1118                     if (data.queryType == QUERY_TYPE_NEWER) {
   1119                         mNewerRequestsProcessed++;
   1120                     } else if (data.queryType == QUERY_TYPE_OLDER) {
   1121                         mOlderRequestsProcessed++;
   1122                     }
   1123 
   1124                     totalAgendaRangeStart = mAdapterInfos.getFirst().start;
   1125                     totalAgendaRangeEnd = mAdapterInfos.getLast().end;
   1126                 } else { // CursorSize == 0
   1127                     QuerySpec querySpec = mQueryQueue.peek();
   1128 
   1129                     // Update Adapter Info with new start and end date range
   1130                     if (!mAdapterInfos.isEmpty()) {
   1131                         DayAdapterInfo first = mAdapterInfos.getFirst();
   1132                         DayAdapterInfo last = mAdapterInfos.getLast();
   1133 
   1134                         if (first.start - 1 <= querySpec.end && querySpec.start < first.start) {
   1135                             first.start = querySpec.start;
   1136                         }
   1137 
   1138                         if (querySpec.start <= last.end + 1 && last.end < querySpec.end) {
   1139                             last.end = querySpec.end;
   1140                         }
   1141 
   1142                         totalAgendaRangeStart = first.start;
   1143                         totalAgendaRangeEnd = last.end;
   1144                     } else {
   1145                         totalAgendaRangeStart = querySpec.start;
   1146                         totalAgendaRangeEnd = querySpec.end;
   1147                     }
   1148 
   1149                     // Update query specification with expanded search range
   1150                     // and maybe rerun query
   1151                     switch (querySpec.queryType) {
   1152                         case QUERY_TYPE_OLDER:
   1153                             totalAgendaRangeStart = querySpec.start;
   1154                             querySpec.start -= MAX_QUERY_DURATION;
   1155                             break;
   1156                         case QUERY_TYPE_NEWER:
   1157                             totalAgendaRangeEnd = querySpec.end;
   1158                             querySpec.end += MAX_QUERY_DURATION;
   1159                             break;
   1160                         case QUERY_TYPE_CLEAN:
   1161                             totalAgendaRangeStart = querySpec.start;
   1162                             totalAgendaRangeEnd = querySpec.end;
   1163                             querySpec.start -= MAX_QUERY_DURATION / 2;
   1164                             querySpec.end += MAX_QUERY_DURATION / 2;
   1165                             break;
   1166                     }
   1167 
   1168                     if (++mEmptyCursorCount > RETRIES_ON_NO_DATA) {
   1169                         // Nothing in the cursor again. Dropping query
   1170                         mQueryQueue.poll();
   1171                     }
   1172                 }
   1173 
   1174                 updateHeaderFooter(totalAgendaRangeStart, totalAgendaRangeEnd);
   1175 
   1176                 // Go over the events and mark the first day after yesterday
   1177                 // that has events in it
   1178                 // If the range of adapters doesn't include yesterday, skip marking it since it will
   1179                 // mark the first day in the adapters.
   1180                 synchronized (mAdapterInfos) {
   1181                     DayAdapterInfo info = mAdapterInfos.getFirst();
   1182                     Time time = new Time(mTimeZone);
   1183                     long now = System.currentTimeMillis();
   1184                     time.set(now);
   1185                     int JulianToday = Time.getJulianDay(now, time.gmtoff);
   1186                     if (info != null && JulianToday >= info.start && JulianToday
   1187                             <= mAdapterInfos.getLast().end) {
   1188                         Iterator<DayAdapterInfo> iter = mAdapterInfos.iterator();
   1189                         boolean foundDay = false;
   1190                         while (iter.hasNext() && !foundDay) {
   1191                             info = iter.next();
   1192                             for (int i = 0; i < info.size; i++) {
   1193                                 if (info.dayAdapter.findJulianDayFromPosition(i) >= JulianToday) {
   1194                                     info.dayAdapter.setAsFirstDayAfterYesterday(i);
   1195                                     foundDay = true;
   1196                                     break;
   1197                                 }
   1198                             }
   1199                         }
   1200                     }
   1201                 }
   1202 
   1203                 // Fire off the next query if any
   1204                 Iterator<QuerySpec> it = mQueryQueue.iterator();
   1205                 while (it.hasNext()) {
   1206                     QuerySpec queryData = it.next();
   1207                     if (queryData.queryType == QUERY_TYPE_CLEAN
   1208                             || !isInRange(queryData.start, queryData.end)) {
   1209                         // Query accepted
   1210                         if (DEBUGLOG) Log.e(TAG, "Query accepted. QueueSize:" + mQueryQueue.size());
   1211                         doQuery(queryData);
   1212                         break;
   1213                     } else {
   1214                         // Query rejected
   1215                         it.remove();
   1216                         if (DEBUGLOG) Log.e(TAG, "Query rejected. QueueSize:" + mQueryQueue.size());
   1217                     }
   1218                 }
   1219             }
   1220             if (BASICLOG) {
   1221                 for (DayAdapterInfo info3 : mAdapterInfos) {
   1222                     Log.e(TAG, "> " + info3.toString());
   1223                 }
   1224             }
   1225         }
   1226 
   1227         /*
   1228          * Update the adapter info array with a the new cursor. Close out old
   1229          * cursors as needed.
   1230          *
   1231          * @return number of rows removed from the beginning
   1232          */
   1233         private int processNewCursor(QuerySpec data, Cursor cursor) {
   1234             synchronized (mAdapterInfos) {
   1235                 // Remove adapter info's from adapterInfos as needed
   1236                 DayAdapterInfo info = pruneAdapterInfo(data.queryType);
   1237                 int listPositionOffset = 0;
   1238                 if (info == null) {
   1239                     info = new DayAdapterInfo(mContext);
   1240                 } else {
   1241                     if (DEBUGLOG)
   1242                         Log.e(TAG, "processNewCursor listPositionOffsetA="
   1243                                 + -info.size);
   1244                     listPositionOffset = -info.size;
   1245                 }
   1246 
   1247                 // Setup adapter info
   1248                 info.start = data.start;
   1249                 info.end = data.end;
   1250                 info.cursor = cursor;
   1251                 info.dayAdapter.changeCursor(info);
   1252                 info.size = info.dayAdapter.getCount();
   1253 
   1254                 // Insert into adapterInfos
   1255                 if (mAdapterInfos.isEmpty()
   1256                         || data.end <= mAdapterInfos.getFirst().start) {
   1257                     mAdapterInfos.addFirst(info);
   1258                     listPositionOffset += info.size;
   1259                 } else if (BASICLOG && data.start < mAdapterInfos.getLast().end) {
   1260                     mAdapterInfos.addLast(info);
   1261                     for (DayAdapterInfo info2 : mAdapterInfos) {
   1262                         Log.e("========== BUG ==", info2.toString());
   1263                     }
   1264                 } else {
   1265                     mAdapterInfos.addLast(info);
   1266                 }
   1267 
   1268                 // Update offsets in adapterInfos
   1269                 mRowCount = 0;
   1270                 for (DayAdapterInfo info3 : mAdapterInfos) {
   1271                     info3.offset = mRowCount;
   1272                     mRowCount += info3.size;
   1273                 }
   1274                 mLastUsedInfo = null;
   1275 
   1276                 return listPositionOffset;
   1277             }
   1278         }
   1279     }
   1280 
   1281     static String getViewTitle(View x) {
   1282         String title = "";
   1283         if (x != null) {
   1284             Object yy = x.getTag();
   1285             if (yy instanceof AgendaAdapter.ViewHolder) {
   1286                 TextView tv = ((AgendaAdapter.ViewHolder) yy).title;
   1287                 if (tv != null) {
   1288                     title = (String) tv.getText();
   1289                 }
   1290             } else if (yy != null) {
   1291                 TextView dateView = ((AgendaByDayAdapter.ViewHolder) yy).dateView;
   1292                 if (dateView != null) {
   1293                     title = (String) dateView.getText();
   1294                 }
   1295             }
   1296         }
   1297         return title;
   1298     }
   1299 
   1300     public void onResume() {
   1301         mTZUpdater.run();
   1302     }
   1303 
   1304     public void setHideDeclinedEvents(boolean hideDeclined) {
   1305         mHideDeclined = hideDeclined;
   1306     }
   1307 
   1308     public void setSelectedView(View v) {
   1309         if (v != null) {
   1310             Object vh = v.getTag();
   1311             if (vh instanceof AgendaAdapter.ViewHolder) {
   1312                 mSelectedVH = (AgendaAdapter.ViewHolder) vh;
   1313                 if (mSelectedInstanceId != mSelectedVH.instanceId) {
   1314                     mSelectedInstanceId = mSelectedVH.instanceId;
   1315                     notifyDataSetChanged();
   1316                 }
   1317             }
   1318         }
   1319     }
   1320 
   1321     public AgendaAdapter.ViewHolder getSelectedViewHolder() {
   1322         return mSelectedVH;
   1323     }
   1324 
   1325     public long getSelectedInstanceId() {
   1326         return mSelectedInstanceId;
   1327     }
   1328 
   1329     public void setSelectedInstanceId(long selectedInstanceId) {
   1330         mSelectedInstanceId = selectedInstanceId;
   1331         mSelectedVH = null;
   1332     }
   1333 
   1334     private long findInstanceIdFromPosition(int position) {
   1335         DayAdapterInfo info = getAdapterInfoByPosition(position);
   1336         if (info != null) {
   1337             return info.dayAdapter.getInstanceId(position - info.offset);
   1338         }
   1339         return -1;
   1340     }
   1341 
   1342     private long findStartTimeFromPosition(int position) {
   1343         DayAdapterInfo info = getAdapterInfoByPosition(position);
   1344         if (info != null) {
   1345             return info.dayAdapter.getStartTime(position - info.offset);
   1346         }
   1347         return -1;
   1348     }
   1349 
   1350 
   1351     private Cursor getCursorByPosition(int position) {
   1352         DayAdapterInfo info = getAdapterInfoByPosition(position);
   1353         if (info != null) {
   1354             return info.cursor;
   1355         }
   1356         return null;
   1357     }
   1358 
   1359     private int getCursorPositionByPosition(int position) {
   1360         DayAdapterInfo info = getAdapterInfoByPosition(position);
   1361         if (info != null) {
   1362             return info.dayAdapter.getCursorPosition(position - info.offset);
   1363         }
   1364         return -1;
   1365     }
   1366 
   1367     // Implementation of HeaderIndexer interface for StickyHeeaderListView
   1368 
   1369     // Returns the location of the day header of a specific event specified in the position
   1370     // in the adapter
   1371     @Override
   1372     public int getHeaderPositionFromItemPosition(int position) {
   1373 
   1374         // For phone configuration, return -1 so there will be no sticky header
   1375         if (!mIsTabletConfig) {
   1376             return -1;
   1377         }
   1378 
   1379         DayAdapterInfo info = getAdapterInfoByPosition(position);
   1380         if (info != null) {
   1381             int pos = info.dayAdapter.getHeaderPosition(position - info.offset);
   1382             return (pos != -1)?(pos + info.offset):-1;
   1383         }
   1384         return -1;
   1385     }
   1386 
   1387     // Returns the number of events for a specific day header
   1388     @Override
   1389     public int getHeaderItemsNumber(int headerPosition) {
   1390         if (headerPosition < 0 || !mIsTabletConfig) {
   1391             return -1;
   1392         }
   1393         DayAdapterInfo info = getAdapterInfoByPosition(headerPosition);
   1394         if (info != null) {
   1395             return info.dayAdapter.getHeaderItemsCount(headerPosition - info.offset);
   1396         }
   1397         return -1;
   1398     }
   1399 
   1400     @Override
   1401     public void OnHeaderHeightChanged(int height) {
   1402         mStickyHeaderSize = height;
   1403     }
   1404 
   1405     public int getStickyHeaderHeight() {
   1406         return mStickyHeaderSize;
   1407     }
   1408 
   1409     public void setScrollState(int state) {
   1410         mListViewScrollState = state;
   1411     }
   1412 }
   1413