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