AgendaWindowAdapter.java revision d2fb20d676ddc5b085dd777765b5f431d067e061
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
17package com.android.calendar.agenda;
18
19import android.content.AsyncQueryHandler;
20import android.content.ContentResolver;
21import android.content.ContentUris;
22import android.content.Context;
23import android.content.res.Resources;
24import android.database.Cursor;
25import android.net.Uri;
26import android.os.Handler;
27import android.provider.CalendarContract;
28import android.provider.CalendarContract.Attendees;
29import android.provider.CalendarContract.Calendars;
30import android.provider.CalendarContract.Instances;
31import android.text.format.DateUtils;
32import android.text.format.Time;
33import android.util.Log;
34import android.view.LayoutInflater;
35import android.view.View;
36import android.view.View.OnClickListener;
37import android.view.ViewGroup;
38import android.widget.AbsListView.OnScrollListener;
39import android.widget.BaseAdapter;
40import android.widget.GridLayout;
41import android.widget.TextView;
42
43import com.android.calendar.CalendarController;
44import com.android.calendar.CalendarController.EventType;
45import com.android.calendar.CalendarController.ViewType;
46import com.android.calendar.R;
47import com.android.calendar.StickyHeaderListView;
48import com.android.calendar.Utils;
49
50import java.util.Formatter;
51import java.util.Iterator;
52import java.util.LinkedList;
53import java.util.Locale;
54import java.util.concurrent.ConcurrentLinkedQueue;
55
56/*
57Bugs 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
65ToDo ToDo ToDo:
66Get design of header and footer from designer
67
68Make scrolling smoother.
69Test for correctness
70Loading speed
71Check for leaks and excessive allocations
72 */
73
74public 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