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