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