AgendaWindowAdapter.java revision ab557bf10630e7d23c20a000bb06e12580852c75
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 com.android.calendar.CalendarController;
20import com.android.calendar.R;
21import com.android.calendar.Utils;
22import com.android.calendar.CalendarController.EventType;
23import com.android.calendar.CalendarController.ViewType;
24import com.android.calendar.StickyHeaderListView;
25
26import android.content.AsyncQueryHandler;
27import android.content.ContentResolver;
28import android.content.ContentUris;
29import android.content.Context;
30import android.content.res.Resources;
31import android.database.Cursor;
32import android.net.Uri;
33import android.os.Handler;
34import android.provider.CalendarContract;
35import android.provider.CalendarContract.Attendees;
36import android.provider.CalendarContract.Calendars;
37import android.provider.CalendarContract.Instances;
38import android.text.format.DateUtils;
39import android.text.format.Time;
40import android.util.Log;
41import android.view.LayoutInflater;
42import android.view.View;
43import android.view.View.OnClickListener;
44import android.view.ViewGroup;
45import android.widget.AbsListView.OnScrollListener;
46import android.widget.BaseAdapter;
47import android.widget.TextView;
48
49import java.util.Formatter;
50import java.util.Iterator;
51import java.util.LinkedList;
52import java.util.Locale;
53import java.util.concurrent.ConcurrentLinkedQueue;
54
55/*
56Bugs 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
64ToDo ToDo ToDo:
65Get design of header and footer from designer
66
67Make scrolling smoother.
68Test for correctness
69Loading speed
70Check for leaks and excessive allocations
71 */
72
73public 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.DISPLAY_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        DayAdapterInfo info = getAdapterInfoByTime(time);
501        int pos = -1;
502        if (info != null) {
503            pos = info.offset + info.dayAdapter.findEventPositionNearestTime(time, id);
504        }
505        if (DEBUGLOG) Log.e(TAG, "findEventPositionNearestTime " + time + " id:" + id + " =" + pos);
506        return pos;
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                actualTime.set(goToTime);
680                CalendarController.getInstance(mContext).sendEvent(this, EventType.UPDATE_TITLE,
681                        actualTime, actualTime, -1, ViewType.CURRENT);
682            }
683            return;
684        }
685
686        // If AllInOneActivity is sending a second GOTO event(in OnResume), ignore it.
687        if (!mCleanQueryInitiated || searchQuery != null) {
688            // Query for a total of MIN_QUERY_DURATION days
689            int endDay = startDay + MIN_QUERY_DURATION;
690
691            mSelectedInstanceId = -1;
692            mCleanQueryInitiated = true;
693            queueQuery(startDay, endDay, goToTime, searchQuery, QUERY_TYPE_CLEAN, id);
694
695            // Pre-fetch more data to overcome a race condition in AgendaListView.shiftSelection
696            // Queuing more data with the goToTime set to the selected time skips the call to
697            // shiftSelection on refresh.
698            mOlderRequests++;
699            queueQuery(0, 0, goToTime, searchQuery, QUERY_TYPE_OLDER, id);
700            mNewerRequests++;
701            queueQuery(0, 0, goToTime, searchQuery, QUERY_TYPE_NEWER, id);
702        }
703    }
704
705    public void close() {
706        mShuttingDown = true;
707        pruneAdapterInfo(QUERY_TYPE_CLEAN);
708        if (mQueryHandler != null) {
709            mQueryHandler.cancelOperation(0);
710        }
711    }
712
713    private DayAdapterInfo pruneAdapterInfo(int queryType) {
714        synchronized (mAdapterInfos) {
715            DayAdapterInfo recycleMe = null;
716            if (!mAdapterInfos.isEmpty()) {
717                if (mAdapterInfos.size() >= MAX_NUM_OF_ADAPTERS) {
718                    if (queryType == QUERY_TYPE_NEWER) {
719                        recycleMe = mAdapterInfos.removeFirst();
720                    } else if (queryType == QUERY_TYPE_OLDER) {
721                        recycleMe = mAdapterInfos.removeLast();
722                        // Keep the size only if the oldest items are removed.
723                        recycleMe.size = 0;
724                    }
725                    if (recycleMe != null) {
726                        if (recycleMe.cursor != null) {
727                            recycleMe.cursor.close();
728                        }
729                        return recycleMe;
730                    }
731                }
732
733                if (mRowCount == 0 || queryType == QUERY_TYPE_CLEAN) {
734                    mRowCount = 0;
735                    int deletedRows = 0;
736                    DayAdapterInfo info;
737                    do {
738                        info = mAdapterInfos.poll();
739                        if (info != null) {
740                            // TODO the following causes ANR's. Do this in a thread.
741                            info.cursor.close();
742                            deletedRows += info.size;
743                            recycleMe = info;
744                        }
745                    } while (info != null);
746
747                    if (recycleMe != null) {
748                        recycleMe.cursor = null;
749                        recycleMe.size = deletedRows;
750                    }
751                }
752            }
753            return recycleMe;
754        }
755    }
756
757    private String buildQuerySelection() {
758        // Respect the preference to show/hide declined events
759
760        if (mHideDeclined) {
761            return Calendars.VISIBLE + "=1 AND "
762                    + Instances.SELF_ATTENDEE_STATUS + "!="
763                    + Attendees.ATTENDEE_STATUS_DECLINED;
764        } else {
765            return Calendars.VISIBLE + "=1";
766        }
767    }
768
769    private Uri buildQueryUri(int start, int end, String searchQuery) {
770        Uri rootUri = searchQuery == null ?
771                Instances.CONTENT_BY_DAY_URI :
772                Instances.CONTENT_SEARCH_BY_DAY_URI;
773        Uri.Builder builder = rootUri.buildUpon();
774        ContentUris.appendId(builder, start);
775        ContentUris.appendId(builder, end);
776        if (searchQuery != null) {
777            builder.appendPath(searchQuery);
778        }
779        return builder.build();
780    }
781
782    private boolean isInRange(int start, int end) {
783        synchronized (mAdapterInfos) {
784            if (mAdapterInfos.isEmpty()) {
785                return false;
786            }
787            return mAdapterInfos.getFirst().start <= start && end <= mAdapterInfos.getLast().end;
788        }
789    }
790
791    private int calculateQueryDuration(int start, int end) {
792        int queryDuration = MAX_QUERY_DURATION;
793        if (mRowCount != 0) {
794            queryDuration = IDEAL_NUM_OF_EVENTS * (end - start + 1) / mRowCount;
795        }
796
797        if (queryDuration > MAX_QUERY_DURATION) {
798            queryDuration = MAX_QUERY_DURATION;
799        } else if (queryDuration < MIN_QUERY_DURATION) {
800            queryDuration = MIN_QUERY_DURATION;
801        }
802
803        return queryDuration;
804    }
805
806    private boolean queueQuery(int start, int end, Time goToTime,
807            String searchQuery, int queryType, long id) {
808        QuerySpec queryData = new QuerySpec(queryType);
809        queryData.goToTime = goToTime;
810        queryData.start = start;
811        queryData.end = end;
812        queryData.searchQuery = searchQuery;
813        queryData.id = id;
814        return queueQuery(queryData);
815    }
816
817    private boolean queueQuery(QuerySpec queryData) {
818        queryData.searchQuery = mSearchQuery;
819        Boolean queuedQuery;
820        synchronized (mQueryQueue) {
821            queuedQuery = false;
822            Boolean doQueryNow = mQueryQueue.isEmpty();
823            mQueryQueue.add(queryData);
824            queuedQuery = true;
825            if (doQueryNow) {
826                doQuery(queryData);
827            }
828        }
829        return queuedQuery;
830    }
831
832    private void doQuery(QuerySpec queryData) {
833        if (!mAdapterInfos.isEmpty()) {
834            int start = mAdapterInfos.getFirst().start;
835            int end = mAdapterInfos.getLast().end;
836            int queryDuration = calculateQueryDuration(start, end);
837            switch(queryData.queryType) {
838                case QUERY_TYPE_OLDER:
839                    queryData.end = start - 1;
840                    queryData.start = queryData.end - queryDuration;
841                    break;
842                case QUERY_TYPE_NEWER:
843                    queryData.start = end + 1;
844                    queryData.end = queryData.start + queryDuration;
845                    break;
846            }
847
848            // By "compacting" cursors, this fixes the disco/ping-pong problem
849            // b/5311977
850            if (mRowCount < 20 && queryData.queryType != QUERY_TYPE_CLEAN) {
851                if (DEBUGLOG) {
852                    Log.e(TAG, "Compacting cursor: mRowCount=" + mRowCount
853                            + " totalStart:" + start
854                            + " totalEnd:" + end
855                            + " query.start:" + queryData.start
856                            + " query.end:" + queryData.end);
857                }
858
859                queryData.queryType = QUERY_TYPE_CLEAN;
860
861                if (queryData.start > start) {
862                    queryData.start = start;
863                }
864                if (queryData.end < end) {
865                    queryData.end = end;
866                }
867            }
868        }
869
870        if (BASICLOG) {
871            Time time = new Time(mTimeZone);
872            time.setJulianDay(queryData.start);
873            Time time2 = new Time(mTimeZone);
874            time2.setJulianDay(queryData.end);
875            Log.v(TAG, "startQuery: " + time.toString() + " to "
876                    + time2.toString() + " then go to " + queryData.goToTime);
877        }
878
879        mQueryHandler.cancelOperation(0);
880        if (BASICLOG) queryData.queryStartMillis = System.nanoTime();
881
882        Uri queryUri = buildQueryUri(
883                queryData.start, queryData.end, queryData.searchQuery);
884        mQueryHandler.startQuery(0, queryData, queryUri,
885                PROJECTION, buildQuerySelection(), null,
886                AGENDA_SORT_ORDER);
887    }
888
889    private String formatDateString(int julianDay) {
890        Time time = new Time(mTimeZone);
891        time.setJulianDay(julianDay);
892        long millis = time.toMillis(false);
893        mStringBuilder.setLength(0);
894        return DateUtils.formatDateRange(mContext, mFormatter, millis, millis,
895                DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE
896                        | DateUtils.FORMAT_ABBREV_MONTH, mTimeZone).toString();
897    }
898
899    private void updateHeaderFooter(final int start, final int end) {
900        mHeaderView.setText(mContext.getString(R.string.show_older_events,
901                formatDateString(start)));
902        mFooterView.setText(mContext.getString(R.string.show_newer_events,
903                formatDateString(end)));
904    }
905
906    private class QueryHandler extends AsyncQueryHandler {
907
908        public QueryHandler(ContentResolver cr) {
909            super(cr);
910        }
911
912        @Override
913        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
914            QuerySpec data = (QuerySpec)cookie;
915            if (BASICLOG) {
916                long queryEndMillis = System.nanoTime();
917                Log.e(TAG, "Query time(ms): "
918                        + (queryEndMillis - data.queryStartMillis) / 1000000
919                        + " Count: " + cursor.getCount());
920            }
921
922            if (data.queryType == QUERY_TYPE_CLEAN) {
923                mCleanQueryInitiated = false;
924            }
925
926            if (mShuttingDown) {
927                cursor.close();
928                return;
929            }
930
931            // Notify Listview of changes and update position
932            int cursorSize = cursor.getCount();
933            if (cursorSize > 0 || mAdapterInfos.isEmpty() || data.queryType == QUERY_TYPE_CLEAN) {
934                final int listPositionOffset = processNewCursor(data, cursor);
935                int newPosition = -1;
936                if (data.goToTime == null) { // Typical Scrolling type query
937                    notifyDataSetChanged();
938                    if (listPositionOffset != 0) {
939                        mAgendaListView.shiftSelection(listPositionOffset);
940                    }
941                } else { // refresh() called. Go to the designated position
942                    final Time goToTime = data.goToTime;
943                    notifyDataSetChanged();
944                    newPosition = findEventPositionNearestTime(goToTime, data.id);
945                    if (newPosition >= 0) {
946                        if (mListViewScrollState == OnScrollListener.SCROLL_STATE_FLING) {
947                            mAgendaListView.smoothScrollBy(0, 0);
948                        }
949                        mAgendaListView.setSelectionFromTop(newPosition + OFF_BY_ONE_BUG,
950                                mStickyHeaderSize);
951                        Time actualTime = new Time(mTimeZone);
952                        actualTime.set(goToTime);
953                        CalendarController.getInstance(mContext).sendEvent(this,
954                                EventType.UPDATE_TITLE, actualTime, actualTime, -1,
955                                ViewType.CURRENT);
956                    }
957                    if (DEBUGLOG) {
958                        Log.e(TAG, "Setting listview to " +
959                                "findEventPositionNearestTime: " + (newPosition + OFF_BY_ONE_BUG));
960                    }
961                }
962
963                // Make sure we change the selected instance Id only on a clean query and we
964                // do not have one set already
965                if (mSelectedInstanceId == -1 && newPosition != -1 &&
966                        data.queryType == QUERY_TYPE_CLEAN) {
967                    if (data.id != -1 || data.goToTime != null) {
968                        mSelectedInstanceId = findInstanceIdFromPosition(newPosition);
969                    }
970                }
971
972                // size == 1 means a fresh query. Possibly after the data changed.
973                // Let's check whether mSelectedInstanceId is still valid.
974                if (mAdapterInfos.size() == 1 && mSelectedInstanceId != -1) {
975                    boolean found = false;
976                    cursor.moveToPosition(-1);
977                    while (cursor.moveToNext()) {
978                        if (mSelectedInstanceId == cursor
979                                .getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID)) {
980                            found = true;
981                            break;
982                        }
983                    };
984
985                    if (!found) {
986                        mSelectedInstanceId = -1;
987                    }
988                }
989
990                // Show the requested event
991                if (mShowEventOnStart && data.queryType == QUERY_TYPE_CLEAN) {
992                    Cursor tempCursor = null;
993                    int tempCursorPosition = -1;
994
995                    // If no valid event is selected , just pick the first one
996                    if (mSelectedInstanceId == -1) {
997                        if (cursor.moveToFirst()) {
998                            mSelectedInstanceId = cursor
999                                    .getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID);
1000                            // Set up a dummy view holder so we have the right all day
1001                            // info when the view is created.
1002                            // TODO determine the full set of what might be useful to
1003                            // know about the selected view and fill it in.
1004                            mSelectedVH = new AgendaAdapter.ViewHolder();
1005                            mSelectedVH.allDay =
1006                                cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0;
1007                            tempCursor = cursor;
1008                        }
1009                    } else if (newPosition != -1) {
1010                         tempCursor = getCursorByPosition(newPosition);
1011                         tempCursorPosition = getCursorPositionByPosition(newPosition);
1012                    }
1013                    if (tempCursor != null) {
1014                        EventInfo event = buildEventInfoFromCursor(tempCursor, tempCursorPosition,
1015                                false);
1016                        CalendarController.getInstance(mContext).sendEventRelatedEvent(this,
1017                                EventType.VIEW_EVENT, event.id, event.begin, event.end, 0, 0, -1);
1018                    }
1019                }
1020            } else {
1021                cursor.close();
1022            }
1023
1024            // Update header and footer
1025            if (!mDoneSettingUpHeaderFooter) {
1026                OnClickListener headerFooterOnClickListener = new OnClickListener() {
1027                    public void onClick(View v) {
1028                        if (v == mHeaderView) {
1029                            queueQuery(new QuerySpec(QUERY_TYPE_OLDER));
1030                        } else {
1031                            queueQuery(new QuerySpec(QUERY_TYPE_NEWER));
1032                        }
1033                    }};
1034                mHeaderView.setOnClickListener(headerFooterOnClickListener);
1035                mFooterView.setOnClickListener(headerFooterOnClickListener);
1036                mAgendaListView.addFooterView(mFooterView);
1037                mDoneSettingUpHeaderFooter = true;
1038            }
1039            synchronized (mQueryQueue) {
1040                int totalAgendaRangeStart = -1;
1041                int totalAgendaRangeEnd = -1;
1042
1043                if (cursorSize != 0) {
1044                    // Remove the query that just completed
1045                    QuerySpec x = mQueryQueue.poll();
1046                    if (BASICLOG && !x.equals(data)) {
1047                        Log.e(TAG, "onQueryComplete - cookie != head of queue");
1048                    }
1049                    mEmptyCursorCount = 0;
1050                    if (data.queryType == QUERY_TYPE_NEWER) {
1051                        mNewerRequestsProcessed++;
1052                    } else if (data.queryType == QUERY_TYPE_OLDER) {
1053                        mOlderRequestsProcessed++;
1054                    }
1055
1056                    totalAgendaRangeStart = mAdapterInfos.getFirst().start;
1057                    totalAgendaRangeEnd = mAdapterInfos.getLast().end;
1058                } else { // CursorSize == 0
1059                    QuerySpec querySpec = mQueryQueue.peek();
1060
1061                    // Update Adapter Info with new start and end date range
1062                    if (!mAdapterInfos.isEmpty()) {
1063                        DayAdapterInfo first = mAdapterInfos.getFirst();
1064                        DayAdapterInfo last = mAdapterInfos.getLast();
1065
1066                        if (first.start - 1 <= querySpec.end && querySpec.start < first.start) {
1067                            first.start = querySpec.start;
1068                        }
1069
1070                        if (querySpec.start <= last.end + 1 && last.end < querySpec.end) {
1071                            last.end = querySpec.end;
1072                        }
1073
1074                        totalAgendaRangeStart = first.start;
1075                        totalAgendaRangeEnd = last.end;
1076                    } else {
1077                        totalAgendaRangeStart = querySpec.start;
1078                        totalAgendaRangeEnd = querySpec.end;
1079                    }
1080
1081                    // Update query specification with expanded search range
1082                    // and maybe rerun query
1083                    switch (querySpec.queryType) {
1084                        case QUERY_TYPE_OLDER:
1085                            totalAgendaRangeStart = querySpec.start;
1086                            querySpec.start -= MAX_QUERY_DURATION;
1087                            break;
1088                        case QUERY_TYPE_NEWER:
1089                            totalAgendaRangeEnd = querySpec.end;
1090                            querySpec.end += MAX_QUERY_DURATION;
1091                            break;
1092                        case QUERY_TYPE_CLEAN:
1093                            totalAgendaRangeStart = querySpec.start;
1094                            totalAgendaRangeEnd = querySpec.end;
1095                            querySpec.start -= MAX_QUERY_DURATION / 2;
1096                            querySpec.end += MAX_QUERY_DURATION / 2;
1097                            break;
1098                    }
1099
1100                    if (++mEmptyCursorCount > RETRIES_ON_NO_DATA) {
1101                        // Nothing in the cursor again. Dropping query
1102                        mQueryQueue.poll();
1103                    }
1104                }
1105
1106                updateHeaderFooter(totalAgendaRangeStart, totalAgendaRangeEnd);
1107
1108                // Go over the events and mark the first day after yesterday
1109                // that has events in it
1110                synchronized (mAdapterInfos) {
1111                    DayAdapterInfo info = mAdapterInfos.getFirst();
1112                    if (info != null) {
1113                        Time time = new Time(mTimeZone);
1114                        long now = System.currentTimeMillis();
1115                        time.set(now);
1116                        int JulianToday = Time.getJulianDay(now, time.gmtoff);
1117                        Iterator<DayAdapterInfo> iter = mAdapterInfos.iterator();
1118                        boolean foundDay = false;
1119                        while (iter.hasNext() && !foundDay) {
1120                            info = iter.next();
1121                            for (int i = 0; i < info.size; i++) {
1122                                if (info.dayAdapter.findJulianDayFromPosition(i) >= JulianToday) {
1123                                    info.dayAdapter.setAsFirstDayAfterYesterday(i);
1124                                    foundDay = true;
1125                                    break;
1126                                }
1127                            }
1128                        }
1129                    }
1130                }
1131
1132                // Fire off the next query if any
1133                Iterator<QuerySpec> it = mQueryQueue.iterator();
1134                while (it.hasNext()) {
1135                    QuerySpec queryData = it.next();
1136                    if (queryData.queryType == QUERY_TYPE_CLEAN
1137                            || !isInRange(queryData.start, queryData.end)) {
1138                        // Query accepted
1139                        if (DEBUGLOG) Log.e(TAG, "Query accepted. QueueSize:" + mQueryQueue.size());
1140                        doQuery(queryData);
1141                        break;
1142                    } else {
1143                        // Query rejected
1144                        it.remove();
1145                        if (DEBUGLOG) Log.e(TAG, "Query rejected. QueueSize:" + mQueryQueue.size());
1146                    }
1147                }
1148            }
1149            if (BASICLOG) {
1150                for (DayAdapterInfo info3 : mAdapterInfos) {
1151                    Log.e(TAG, "> " + info3.toString());
1152                }
1153            }
1154        }
1155
1156        /*
1157         * Update the adapter info array with a the new cursor. Close out old
1158         * cursors as needed.
1159         *
1160         * @return number of rows removed from the beginning
1161         */
1162        private int processNewCursor(QuerySpec data, Cursor cursor) {
1163            synchronized (mAdapterInfos) {
1164                // Remove adapter info's from adapterInfos as needed
1165                DayAdapterInfo info = pruneAdapterInfo(data.queryType);
1166                int listPositionOffset = 0;
1167                if (info == null) {
1168                    info = new DayAdapterInfo(mContext);
1169                } else {
1170                    if (DEBUGLOG)
1171                        Log.e(TAG, "processNewCursor listPositionOffsetA="
1172                                + -info.size);
1173                    listPositionOffset = -info.size;
1174                }
1175
1176                // Setup adapter info
1177                info.start = data.start;
1178                info.end = data.end;
1179                info.cursor = cursor;
1180                info.dayAdapter.changeCursor(info);
1181                info.size = info.dayAdapter.getCount();
1182
1183                // Insert into adapterInfos
1184                if (mAdapterInfos.isEmpty()
1185                        || data.end <= mAdapterInfos.getFirst().start) {
1186                    mAdapterInfos.addFirst(info);
1187                    listPositionOffset += info.size;
1188                } else if (BASICLOG && data.start < mAdapterInfos.getLast().end) {
1189                    mAdapterInfos.addLast(info);
1190                    for (DayAdapterInfo info2 : mAdapterInfos) {
1191                        Log.e("========== BUG ==", info2.toString());
1192                    }
1193                } else {
1194                    mAdapterInfos.addLast(info);
1195                }
1196
1197                // Update offsets in adapterInfos
1198                mRowCount = 0;
1199                for (DayAdapterInfo info3 : mAdapterInfos) {
1200                    info3.offset = mRowCount;
1201                    mRowCount += info3.size;
1202                }
1203                mLastUsedInfo = null;
1204
1205                return listPositionOffset;
1206            }
1207        }
1208    }
1209
1210    static String getViewTitle(View x) {
1211        String title = "";
1212        if (x != null) {
1213            Object yy = x.getTag();
1214            if (yy instanceof AgendaAdapter.ViewHolder) {
1215                TextView tv = ((AgendaAdapter.ViewHolder) yy).title;
1216                if (tv != null) {
1217                    title = (String) tv.getText();
1218                }
1219            } else if (yy != null) {
1220                TextView dateView = ((AgendaByDayAdapter.ViewHolder) yy).dateView;
1221                if (dateView != null) {
1222                    title = (String) dateView.getText();
1223                }
1224            }
1225        }
1226        return title;
1227    }
1228
1229    public void onResume() {
1230        mTZUpdater.run();
1231    }
1232
1233    public void setHideDeclinedEvents(boolean hideDeclined) {
1234        mHideDeclined = hideDeclined;
1235    }
1236
1237    public void setSelectedView(View v) {
1238        if (v != null) {
1239            Object vh = v.getTag();
1240            if (vh instanceof AgendaAdapter.ViewHolder) {
1241                mSelectedVH = (AgendaAdapter.ViewHolder) vh;
1242                if (mSelectedInstanceId != mSelectedVH.instanceId) {
1243                    mSelectedInstanceId = mSelectedVH.instanceId;
1244                    notifyDataSetChanged();
1245                }
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