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