AgendaWindowAdapter.java revision 37f12e5cee7ed2d354e9366bd6d8e15d1a934f2a
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.StickyHeaderListView;
24
25import android.content.AsyncQueryHandler;
26import android.content.ContentResolver;
27import android.content.ContentUris;
28import android.content.Context;
29import android.content.res.Resources;
30import android.database.Cursor;
31import android.net.Uri;
32import android.provider.Calendar;
33import android.provider.Calendar.Attendees;
34import android.provider.Calendar.Calendars;
35import android.provider.Calendar.Instances;
36import android.text.format.DateUtils;
37import android.text.format.Time;
38import android.util.Log;
39import android.view.LayoutInflater;
40import android.view.View;
41import android.view.View.OnClickListener;
42import android.view.ViewGroup;
43import android.widget.BaseAdapter;
44import android.widget.TextView;
45
46import java.util.Formatter;
47import java.util.Iterator;
48import java.util.LinkedList;
49import java.util.Locale;
50import java.util.concurrent.ConcurrentLinkedQueue;
51
52/*
53Bugs Bugs Bugs:
54- At rotation and launch time, the initial position is not set properly. This code is calling
55 listview.setSelection() in 2 rapid secessions but it dropped or didn't process the first one.
56- Scroll using trackball isn't repositioning properly after a new adapter is added.
57- Track ball clicks at the header/footer doesn't work.
58- Potential ping pong effect if the prefetch window is big and data is limited
59- Add index in calendar provider
60
61ToDo ToDo ToDo:
62Get design of header and footer from designer
63
64Make scrolling smoother.
65Test for correctness
66Loading speed
67Check for leaks and excessive allocations
68 */
69
70public class AgendaWindowAdapter extends BaseAdapter
71    implements StickyHeaderListView.HeaderIndexer{
72
73    static final boolean BASICLOG = false;
74    static final boolean DEBUGLOG = false;
75    private static final String TAG = "AgendaWindowAdapter";
76
77    private static final String AGENDA_SORT_ORDER =
78            Calendar.Instances.START_DAY + " ASC, " +
79            Calendar.Instances.BEGIN + " ASC, " +
80            Calendar.Events.TITLE + " ASC";
81
82    public static final int INDEX_INSTANCE_ID = 0;
83    public static final int INDEX_TITLE = 1;
84    public static final int INDEX_EVENT_LOCATION = 2;
85    public static final int INDEX_ALL_DAY = 3;
86    public static final int INDEX_HAS_ALARM = 4;
87    public static final int INDEX_COLOR = 5;
88    public static final int INDEX_RRULE = 6;
89    public static final int INDEX_BEGIN = 7;
90    public static final int INDEX_END = 8;
91    public static final int INDEX_EVENT_ID = 9;
92    public static final int INDEX_START_DAY = 10;
93    public static final int INDEX_END_DAY = 11;
94    public static final int INDEX_SELF_ATTENDEE_STATUS = 12;
95
96    private static final String[] PROJECTION = new String[] {
97            Instances._ID, // 0
98            Instances.TITLE, // 1
99            Instances.EVENT_LOCATION, // 2
100            Instances.ALL_DAY, // 3
101            Instances.HAS_ALARM, // 4
102            Instances.CALENDAR_COLOR, // 5
103            Instances.RRULE, // 6
104            Instances.BEGIN, // 7
105            Instances.END, // 8
106            Instances.EVENT_ID, // 9
107            Instances.START_DAY, // 10 Julian start day
108            Instances.END_DAY, // 11 Julian end day
109            Instances.SELF_ATTENDEE_STATUS, // 12
110    };
111
112    // Listview may have a bug where the index/position is not consistent when there's a header.
113    // position == positionInListView - OFF_BY_ONE_BUG
114    // TODO Need to look into this.
115    private static final int OFF_BY_ONE_BUG = 1;
116    private static final int MAX_NUM_OF_ADAPTERS = 5;
117    private static final int IDEAL_NUM_OF_EVENTS = 50;
118    private static final int MIN_QUERY_DURATION = 7; // days
119    private static final int MAX_QUERY_DURATION = 60; // days
120    private static final int PREFETCH_BOUNDARY = 1;
121
122    /** Times to auto-expand/retry query after getting no data */
123    private static final int RETRIES_ON_NO_DATA = 1;
124
125    private Context mContext;
126    private Resources mResources;
127    private QueryHandler mQueryHandler;
128    private AgendaListView mAgendaListView;
129
130    /** The sum of the rows in all the adapters */
131    private int mRowCount;
132
133    /** The number of times we have queried and gotten no results back */
134    private int mEmptyCursorCount;
135
136    /** Cached value of the last used adapter */
137    private DayAdapterInfo mLastUsedInfo;
138
139    private final LinkedList<DayAdapterInfo> mAdapterInfos =
140            new LinkedList<DayAdapterInfo>();
141    private final ConcurrentLinkedQueue<QuerySpec> mQueryQueue =
142            new ConcurrentLinkedQueue<QuerySpec>();
143    private TextView mHeaderView;
144    private TextView mFooterView;
145    private boolean mDoneSettingUpHeaderFooter = false;
146
147    /**
148     * When the user scrolled to the top, a query will be made for older events
149     * and this will be incremented. Don't make more requests if
150     * mOlderRequests > mOlderRequestsProcessed.
151     */
152    private int mOlderRequests;
153
154    /** Number of "older" query that has been processed. */
155    private int mOlderRequestsProcessed;
156
157    /**
158     * When the user scrolled to the bottom, a query will be made for newer
159     * events and this will be incremented. Don't make more requests if
160     * mNewerRequests > mNewerRequestsProcessed.
161     */
162    private int mNewerRequests;
163
164    /** Number of "newer" query that has been processed. */
165    private int mNewerRequestsProcessed;
166
167    // Note: Formatter is not thread safe. Fine for now as it is only used by the main thread.
168    private Formatter mFormatter;
169    private StringBuilder mStringBuilder;
170    private String mTimeZone;
171
172
173    // defines if to pop-up the current event when the agenda is first shown
174    private boolean mShowEventOnStart;
175
176    private Runnable mTZUpdater = new Runnable() {
177        @Override
178        public void run() {
179            mTimeZone = Utils.getTimeZone(mContext, this);
180            notifyDataSetChanged();
181        }
182    };
183
184    private boolean mShuttingDown;
185    private boolean mHideDeclined;
186
187    /** The current search query, or null if none */
188    private String mSearchQuery;
189
190    private long mSelectedInstanceId = -1;
191
192    private final int mSelectedAgendaItemColor;
193
194    // Types of Query
195    private static final int QUERY_TYPE_OLDER = 0; // Query for older events
196    private static final int QUERY_TYPE_NEWER = 1; // Query for newer events
197    private static final int QUERY_TYPE_CLEAN = 2; // Delete everything and query around a date
198
199    private static class QuerySpec {
200        long queryStartMillis;
201        Time goToTime;
202        int start;
203        int end;
204        String searchQuery;
205        int queryType;
206
207        public QuerySpec(int queryType) {
208            this.queryType = queryType;
209        }
210
211        @Override
212        public int hashCode() {
213            final int prime = 31;
214            int result = 1;
215            result = prime * result + end;
216            result = prime * result + (int) (queryStartMillis ^ (queryStartMillis >>> 32));
217            result = prime * result + queryType;
218            result = prime * result + start;
219            result = prime * result + searchQuery.hashCode();
220            if (goToTime != null) {
221                long goToTimeMillis = goToTime.toMillis(false);
222                result = prime * result + (int) (goToTimeMillis ^ (goToTimeMillis >>> 32));
223            }
224            return result;
225        }
226
227        @Override
228        public boolean equals(Object obj) {
229            if (this == obj) return true;
230            if (obj == null) return false;
231            if (getClass() != obj.getClass()) return false;
232            QuerySpec other = (QuerySpec) obj;
233            if (end != other.end || queryStartMillis != other.queryStartMillis
234                    || queryType != other.queryType || start != other.start
235                    || Utils.equals(searchQuery, other.searchQuery)) {
236                return false;
237            }
238
239            if (goToTime != null) {
240                if (goToTime.toMillis(false) != other.goToTime.toMillis(false)) {
241                    return false;
242                }
243            } else {
244                if (other.goToTime != null) {
245                    return false;
246                }
247            }
248            return true;
249        }
250    }
251
252    static class EventInfo {
253        long begin;
254        long end;
255        long id;
256        int startDay;
257    }
258
259    static class DayAdapterInfo {
260        Cursor cursor;
261        AgendaByDayAdapter dayAdapter;
262        int start; // start day of the cursor's coverage
263        int end; // end day of the cursor's coverage
264        int offset; // offset in position in the list view
265        int size; // dayAdapter.getCount()
266
267        public DayAdapterInfo(Context context) {
268            dayAdapter = new AgendaByDayAdapter(context);
269        }
270
271        @Override
272        public String toString() {
273            // Static class, so the time in this toString will not reflect the
274            // home tz settings. This should only affect debugging.
275            Time time = new Time();
276            StringBuilder sb = new StringBuilder();
277            time.setJulianDay(start);
278            time.normalize(false);
279            sb.append("Start:").append(time.toString());
280            time.setJulianDay(end);
281            time.normalize(false);
282            sb.append(" End:").append(time.toString());
283            sb.append(" Offset:").append(offset);
284            sb.append(" Size:").append(size);
285            return sb.toString();
286        }
287    }
288
289    public AgendaWindowAdapter(Context context,
290            AgendaListView agendaListView, boolean showEventOnStart) {
291        mContext = context;
292        mResources = context.getResources();
293        mSelectedAgendaItemColor = mResources.getColor(R.color.activated);
294
295        mTimeZone = Utils.getTimeZone(context, mTZUpdater);
296        mAgendaListView = agendaListView;
297        mQueryHandler = new QueryHandler(context.getContentResolver());
298
299        mStringBuilder = new StringBuilder(50);
300        mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
301
302        mShowEventOnStart = showEventOnStart;
303
304        mSearchQuery = null;
305
306        LayoutInflater inflater = (LayoutInflater) context
307                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
308        mHeaderView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
309        mFooterView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
310        mHeaderView.setText(R.string.loading);
311        mAgendaListView.addHeaderView(mHeaderView);
312    }
313
314    // Method in Adapter
315    @Override
316    public int getViewTypeCount() {
317        return AgendaByDayAdapter.TYPE_LAST;
318    }
319
320    // Method in BaseAdapter
321    @Override
322    public boolean areAllItemsEnabled() {
323        return false;
324    }
325
326    // Method in Adapter
327    @Override
328    public int getItemViewType(int position) {
329        DayAdapterInfo info = getAdapterInfoByPosition(position);
330        if (info != null) {
331            return info.dayAdapter.getItemViewType(position - info.offset);
332        } else {
333            return -1;
334        }
335    }
336
337    // Method in BaseAdapter
338    @Override
339    public boolean isEnabled(int position) {
340        DayAdapterInfo info = getAdapterInfoByPosition(position);
341        if (info != null) {
342            return info.dayAdapter.isEnabled(position - info.offset);
343        } else {
344            return false;
345        }
346    }
347
348    // Abstract Method in BaseAdapter
349    public int getCount() {
350        return mRowCount;
351    }
352
353    // Abstract Method in BaseAdapter
354    public Object getItem(int position) {
355        DayAdapterInfo info = getAdapterInfoByPosition(position);
356        if (info != null) {
357            return info.dayAdapter.getItem(position - info.offset);
358        } else {
359            return null;
360        }
361    }
362
363    // Method in BaseAdapter
364    @Override
365    public boolean hasStableIds() {
366        return true;
367    }
368
369    // Abstract Method in BaseAdapter
370    public long getItemId(int position) {
371        DayAdapterInfo info = getAdapterInfoByPosition(position);
372        if (info != null) {
373            return ((position - info.offset) << 20) + info.start ;
374        } else {
375            return -1;
376        }
377    }
378
379    // Abstract Method in BaseAdapter
380    public View getView(int position, View convertView, ViewGroup parent) {
381        if (position >= (mRowCount - PREFETCH_BOUNDARY)
382                && mNewerRequests <= mNewerRequestsProcessed) {
383            if (DEBUGLOG) Log.e(TAG, "queryForNewerEvents: ");
384            mNewerRequests++;
385            queueQuery(new QuerySpec(QUERY_TYPE_NEWER));
386        }
387
388        if (position < PREFETCH_BOUNDARY
389                && mOlderRequests <= mOlderRequestsProcessed) {
390            if (DEBUGLOG) Log.e(TAG, "queryForOlderEvents: ");
391            mOlderRequests++;
392            queueQuery(new QuerySpec(QUERY_TYPE_OLDER));
393        }
394
395        final View v;
396        DayAdapterInfo info = getAdapterInfoByPosition(position);
397        if (info != null) {
398            v = info.dayAdapter.getView(position - info.offset, convertView,
399                    parent);
400        } else {
401            //TODO
402            Log.e(TAG, "BUG: getAdapterInfoByPosition returned null!!! " + position);
403            TextView tv = new TextView(mContext);
404            tv.setText("Bug! " + position);
405            v = tv;
406        }
407
408        // Show selected marker if this is item is selected
409        boolean selected = false;
410        Object yy = v.getTag();
411        if (yy instanceof AgendaAdapter.ViewHolder) {
412            AgendaAdapter.ViewHolder vh = (AgendaAdapter.ViewHolder) yy;
413            selected = mSelectedInstanceId == vh.instanceId;
414            vh.selectedMarker.setVisibility((selected && mShowEventOnStart) ?
415                    View.VISIBLE : View.GONE);
416            if (selected) {
417                v.setBackgroundColor(mSelectedAgendaItemColor);
418            }
419        }
420
421        if (DEBUGLOG) {
422            Log.e(TAG, "getView " + position + " = " + getViewTitle(v));
423        }
424        return v;
425    }
426
427    private int findDayPositionNearestTime(Time time) {
428        if (DEBUGLOG) Log.e(TAG, "findDayPositionNearestTime " + time);
429
430        DayAdapterInfo info = getAdapterInfoByTime(time);
431        if (info != null) {
432            return info.offset + info.dayAdapter.findDayPositionNearestTime(time);
433        } else {
434            return -1;
435        }
436    }
437
438    private DayAdapterInfo getAdapterInfoByPosition(int position) {
439        synchronized (mAdapterInfos) {
440            if (mLastUsedInfo != null && mLastUsedInfo.offset <= position
441                    && position < (mLastUsedInfo.offset + mLastUsedInfo.size)) {
442                return mLastUsedInfo;
443            }
444            for (DayAdapterInfo info : mAdapterInfos) {
445                if (info.offset <= position
446                        && position < (info.offset + info.size)) {
447                    mLastUsedInfo = info;
448                    return info;
449                }
450            }
451        }
452        return null;
453    }
454
455    private DayAdapterInfo getAdapterInfoByTime(Time time) {
456        if (DEBUGLOG) Log.e(TAG, "getAdapterInfoByTime " + time.toString());
457
458        Time tmpTime = new Time(time);
459        long timeInMillis = tmpTime.normalize(true);
460        int day = Time.getJulianDay(timeInMillis, tmpTime.gmtoff);
461        synchronized (mAdapterInfos) {
462            for (DayAdapterInfo info : mAdapterInfos) {
463                if (info.start <= day && day < info.end) {
464                    return info;
465                }
466            }
467        }
468        return null;
469    }
470
471    public EventInfo getEventByPosition(final int positionInListView) {
472        if (DEBUGLOG) Log.e(TAG, "getEventByPosition " + positionInListView);
473
474        final int positionInAdapter = positionInListView - OFF_BY_ONE_BUG;
475        DayAdapterInfo info = getAdapterInfoByPosition(positionInAdapter);
476        if (info == null) {
477            return null;
478        }
479
480        int cursorPosition = info.dayAdapter.getCursorPosition(positionInAdapter - info.offset);
481        if (cursorPosition == Integer.MIN_VALUE) {
482            return null;
483        }
484
485        boolean isDayHeader = false;
486        if (cursorPosition < 0) {
487            cursorPosition = -cursorPosition;
488            isDayHeader = true;
489        }
490
491        if (cursorPosition < info.cursor.getCount()) {
492            info.cursor.moveToPosition(cursorPosition);
493            return buildEventInfoFromCursor(info.cursor, isDayHeader);
494        }
495        return null;
496    }
497
498    private EventInfo buildEventInfoFromCursor(final Cursor cursor, boolean isDayHeader) {
499        EventInfo event = new EventInfo();
500        event.begin = cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN);
501        event.startDay = cursor.getInt(AgendaWindowAdapter.INDEX_START_DAY);
502
503        boolean allDay = cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0;
504        if (allDay) { // UTC
505            Time time = new Time(mTimeZone);
506            time.setJulianDay(Time.getJulianDay(event.begin, 0));
507            event.begin = time.toMillis(false /* use isDst */);
508        } else if (isDayHeader) { // Trim to midnight.
509            Time time = new Time(mTimeZone);
510            time.set(event.begin);
511            time.hour = 0;
512            time.minute = 0;
513            time.second = 0;
514            event.begin = time.toMillis(false /* use isDst */);
515        }
516
517        if (!isDayHeader) {
518            if (allDay) {
519                Time time = new Time(mTimeZone);
520                time.setJulianDay(Time.getJulianDay(event.end, 0));
521                event.end = time.toMillis(false /* use isDst */);
522            } else {
523                event.end = cursor.getLong(AgendaWindowAdapter.INDEX_END);
524            }
525
526            event.id = cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID);
527        }
528        return event;
529    }
530
531    public void refresh(Time goToTime, long id, String searchQuery, boolean forced) {
532        if (searchQuery != null) {
533            mSearchQuery = searchQuery;
534        }
535
536        if (DEBUGLOG) {
537            Log.e(TAG, "refresh " + goToTime.toString() + (forced ? " forced" : " not forced"));
538        }
539
540        int startDay = Time.getJulianDay(goToTime.toMillis(false), goToTime.gmtoff);
541
542        if (!forced && isInRange(startDay, startDay)) {
543            // No need to re-query
544            if (!mAgendaListView.isEventVisible(goToTime, id)) {
545                mAgendaListView.setSelection(findDayPositionNearestTime(goToTime) + OFF_BY_ONE_BUG);
546            }
547            return;
548        }
549
550        // Query for a total of MIN_QUERY_DURATION days
551        int endDay = startDay + MIN_QUERY_DURATION;
552
553        queueQuery(startDay, endDay, goToTime, searchQuery, QUERY_TYPE_CLEAN);
554    }
555
556    public void close() {
557        mShuttingDown = true;
558        pruneAdapterInfo(QUERY_TYPE_CLEAN);
559        if (mQueryHandler != null) {
560            mQueryHandler.cancelOperation(0);
561        }
562    }
563
564    private DayAdapterInfo pruneAdapterInfo(int queryType) {
565        synchronized (mAdapterInfos) {
566            DayAdapterInfo recycleMe = null;
567            if (!mAdapterInfos.isEmpty()) {
568                if (mAdapterInfos.size() >= MAX_NUM_OF_ADAPTERS) {
569                    if (queryType == QUERY_TYPE_NEWER) {
570                        recycleMe = mAdapterInfos.removeFirst();
571                    } else if (queryType == QUERY_TYPE_OLDER) {
572                        recycleMe = mAdapterInfos.removeLast();
573                        // Keep the size only if the oldest items are removed.
574                        recycleMe.size = 0;
575                    }
576                    if (recycleMe != null) {
577                        if (recycleMe.cursor != null) {
578                            recycleMe.cursor.close();
579                        }
580                        return recycleMe;
581                    }
582                }
583
584                if (mRowCount == 0 || queryType == QUERY_TYPE_CLEAN) {
585                    mRowCount = 0;
586                    int deletedRows = 0;
587                    DayAdapterInfo info;
588                    do {
589                        info = mAdapterInfos.poll();
590                        if (info != null) {
591                            // TODO the following causes ANR's. Do this in a thread.
592                            info.cursor.close();
593                            deletedRows += info.size;
594                            recycleMe = info;
595                        }
596                    } while (info != null);
597
598                    if (recycleMe != null) {
599                        recycleMe.cursor = null;
600                        recycleMe.size = deletedRows;
601                    }
602                }
603            }
604            return recycleMe;
605        }
606    }
607
608    private String buildQuerySelection() {
609        // Respect the preference to show/hide declined events
610
611        if (mHideDeclined) {
612            return Calendars.VISIBLE + "=1 AND "
613                    + Instances.SELF_ATTENDEE_STATUS + "!="
614                    + Attendees.ATTENDEE_STATUS_DECLINED;
615        } else {
616            return Calendars.VISIBLE + "=1";
617        }
618    }
619
620    private Uri buildQueryUri(int start, int end, String searchQuery) {
621        Uri rootUri = searchQuery == null ?
622                Instances.CONTENT_BY_DAY_URI :
623                Instances.CONTENT_SEARCH_BY_DAY_URI;
624        Uri.Builder builder = rootUri.buildUpon();
625        ContentUris.appendId(builder, start);
626        ContentUris.appendId(builder, end);
627        if (searchQuery != null) {
628            builder.appendPath(searchQuery);
629        }
630        return builder.build();
631    }
632
633    private boolean isInRange(int start, int end) {
634        synchronized (mAdapterInfos) {
635            if (mAdapterInfos.isEmpty()) {
636                return false;
637            }
638            return mAdapterInfos.getFirst().start <= start && end <= mAdapterInfos.getLast().end;
639        }
640    }
641
642    private int calculateQueryDuration(int start, int end) {
643        int queryDuration = MAX_QUERY_DURATION;
644        if (mRowCount != 0) {
645            queryDuration = IDEAL_NUM_OF_EVENTS * (end - start + 1) / mRowCount;
646        }
647
648        if (queryDuration > MAX_QUERY_DURATION) {
649            queryDuration = MAX_QUERY_DURATION;
650        } else if (queryDuration < MIN_QUERY_DURATION) {
651            queryDuration = MIN_QUERY_DURATION;
652        }
653
654        return queryDuration;
655    }
656
657    private boolean queueQuery(int start, int end, Time goToTime,
658            String searchQuery, int queryType) {
659        QuerySpec queryData = new QuerySpec(queryType);
660        queryData.goToTime = goToTime;
661        queryData.start = start;
662        queryData.end = end;
663        queryData.searchQuery = searchQuery;
664        return queueQuery(queryData);
665    }
666
667    private boolean queueQuery(QuerySpec queryData) {
668        queryData.searchQuery = mSearchQuery;
669        Boolean queuedQuery;
670        synchronized (mQueryQueue) {
671            queuedQuery = false;
672            Boolean doQueryNow = mQueryQueue.isEmpty();
673            mQueryQueue.add(queryData);
674            queuedQuery = true;
675            if (doQueryNow) {
676                doQuery(queryData);
677            }
678        }
679        return queuedQuery;
680    }
681
682    private void doQuery(QuerySpec queryData) {
683        if (!mAdapterInfos.isEmpty()) {
684            int start = mAdapterInfos.getFirst().start;
685            int end = mAdapterInfos.getLast().end;
686            int queryDuration = calculateQueryDuration(start, end);
687            switch(queryData.queryType) {
688                case QUERY_TYPE_OLDER:
689                    queryData.end = start - 1;
690                    queryData.start = queryData.end - queryDuration;
691                    break;
692                case QUERY_TYPE_NEWER:
693                    queryData.start = end + 1;
694                    queryData.end = queryData.start + queryDuration;
695                    break;
696            }
697        }
698
699        if (BASICLOG) {
700            Time time = new Time(mTimeZone);
701            time.setJulianDay(queryData.start);
702            Time time2 = new Time(mTimeZone);
703            time2.setJulianDay(queryData.end);
704            Log.v(TAG, "startQuery: " + time.toString() + " to "
705                    + time2.toString() + " then go to " + queryData.goToTime);
706        }
707
708        mQueryHandler.cancelOperation(0);
709        if (BASICLOG) queryData.queryStartMillis = System.nanoTime();
710
711        Uri queryUri = buildQueryUri(
712                queryData.start, queryData.end, queryData.searchQuery);
713        mQueryHandler.startQuery(0, queryData, queryUri,
714                PROJECTION, buildQuerySelection(), null,
715                AGENDA_SORT_ORDER);
716    }
717
718    private String formatDateString(int julianDay) {
719        Time time = new Time(mTimeZone);
720        time.setJulianDay(julianDay);
721        long millis = time.toMillis(false);
722        mStringBuilder.setLength(0);
723        return DateUtils.formatDateRange(mContext, mFormatter, millis, millis,
724                DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE
725                        | DateUtils.FORMAT_ABBREV_MONTH, mTimeZone).toString();
726    }
727
728    private void updateHeaderFooter(final int start, final int end) {
729        mHeaderView.setText(mContext.getString(R.string.show_older_events,
730                formatDateString(start)));
731        mFooterView.setText(mContext.getString(R.string.show_newer_events,
732                formatDateString(end)));
733    }
734
735    private class QueryHandler extends AsyncQueryHandler {
736
737        public QueryHandler(ContentResolver cr) {
738            super(cr);
739        }
740
741        @Override
742        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
743            QuerySpec data = (QuerySpec)cookie;
744            if (BASICLOG) {
745                long queryEndMillis = System.nanoTime();
746                Log.e(TAG, "Query time(ms): "
747                        + (queryEndMillis - data.queryStartMillis) / 1000000
748                        + " Count: " + cursor.getCount());
749            }
750
751            if (mShuttingDown) {
752                cursor.close();
753                return;
754            }
755
756            // Notify Listview of changes and update position
757            int cursorSize = cursor.getCount();
758            if (cursorSize > 0 || mAdapterInfos.isEmpty() || data.queryType == QUERY_TYPE_CLEAN) {
759                final int listPositionOffset = processNewCursor(data, cursor);
760                if (data.goToTime == null) { // Typical Scrolling type query
761                    notifyDataSetChanged();
762                    if (listPositionOffset != 0) {
763                        mAgendaListView.shiftSelection(listPositionOffset);
764                    }
765                } else { // refresh() called. Go to the designated position
766                    final Time goToTime = data.goToTime;
767                    notifyDataSetChanged();
768                    int newPosition = findDayPositionNearestTime(goToTime);
769                    if (newPosition >= 0) {
770                        mAgendaListView.setSelection(newPosition + OFF_BY_ONE_BUG);
771                    }
772                    if (DEBUGLOG) {
773                        Log.e(TAG, "Setting listview to " +
774                                "findDayPositionNearestTime: " + (newPosition + OFF_BY_ONE_BUG));
775                    }
776                }
777
778                // size == 1 means a fresh query. Possibly after the data changed.
779                // Let's check whether mSelectedInstanceId is still valid.
780                if (mAdapterInfos.size() == 1 && mSelectedInstanceId != -1) {
781                    boolean found = false;
782                    cursor.moveToPosition(-1);
783                    while (cursor.moveToNext()) {
784                        if (mSelectedInstanceId == cursor
785                                .getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID)) {
786                            found = true;
787                            break;
788                        }
789                    };
790
791                    if (!found) {
792                        mSelectedInstanceId = -1;
793                    }
794                }
795
796                if (mSelectedInstanceId == -1 && cursor.moveToFirst()) {
797                    mSelectedInstanceId = cursor.getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID);
798
799                    EventInfo event = buildEventInfoFromCursor(cursor, false);
800                    if (mShowEventOnStart) {
801                        CalendarController.getInstance(mContext).sendEventRelatedEvent(this,
802                                EventType.VIEW_EVENT, event.id, event.begin, event.end, 0, 0, -1);
803                    }
804                }
805            } else {
806                cursor.close();
807            }
808
809            // Update header and footer
810            if (!mDoneSettingUpHeaderFooter) {
811                OnClickListener headerFooterOnClickListener = new OnClickListener() {
812                    public void onClick(View v) {
813                        if (v == mHeaderView) {
814                            queueQuery(new QuerySpec(QUERY_TYPE_OLDER));
815                        } else {
816                            queueQuery(new QuerySpec(QUERY_TYPE_NEWER));
817                        }
818                    }};
819                mHeaderView.setOnClickListener(headerFooterOnClickListener);
820                mFooterView.setOnClickListener(headerFooterOnClickListener);
821                mAgendaListView.addFooterView(mFooterView);
822                mDoneSettingUpHeaderFooter = true;
823            }
824            synchronized (mQueryQueue) {
825                int totalAgendaRangeStart = -1;
826                int totalAgendaRangeEnd = -1;
827
828                if (cursorSize != 0) {
829                    // Remove the query that just completed
830                    QuerySpec x = mQueryQueue.poll();
831                    if (BASICLOG && !x.equals(data)) {
832                        Log.e(TAG, "onQueryComplete - cookie != head of queue");
833                    }
834                    mEmptyCursorCount = 0;
835                    if (data.queryType == QUERY_TYPE_NEWER) {
836                        mNewerRequestsProcessed++;
837                    } else if (data.queryType == QUERY_TYPE_OLDER) {
838                        mOlderRequestsProcessed++;
839                    }
840
841                    totalAgendaRangeStart = mAdapterInfos.getFirst().start;
842                    totalAgendaRangeEnd = mAdapterInfos.getLast().end;
843                } else { // CursorSize == 0
844                    QuerySpec querySpec = mQueryQueue.peek();
845
846                    // Update Adapter Info with new start and end date range
847                    if (!mAdapterInfos.isEmpty()) {
848                        DayAdapterInfo first = mAdapterInfos.getFirst();
849                        DayAdapterInfo last = mAdapterInfos.getLast();
850
851                        if (first.start - 1 <= querySpec.end && querySpec.start < first.start) {
852                            first.start = querySpec.start;
853                        }
854
855                        if (querySpec.start <= last.end + 1 && last.end < querySpec.end) {
856                            last.end = querySpec.end;
857                        }
858
859                        totalAgendaRangeStart = first.start;
860                        totalAgendaRangeEnd = last.end;
861                    } else {
862                        totalAgendaRangeStart = querySpec.start;
863                        totalAgendaRangeEnd = querySpec.end;
864                    }
865
866                    // Update query specification with expanded search range
867                    // and maybe rerun query
868                    switch (querySpec.queryType) {
869                        case QUERY_TYPE_OLDER:
870                            totalAgendaRangeStart = querySpec.start;
871                            querySpec.start -= MAX_QUERY_DURATION;
872                            break;
873                        case QUERY_TYPE_NEWER:
874                            totalAgendaRangeEnd = querySpec.end;
875                            querySpec.end += MAX_QUERY_DURATION;
876                            break;
877                        case QUERY_TYPE_CLEAN:
878                            totalAgendaRangeStart = querySpec.start;
879                            totalAgendaRangeEnd = querySpec.end;
880                            querySpec.start -= MAX_QUERY_DURATION / 2;
881                            querySpec.end += MAX_QUERY_DURATION / 2;
882                            break;
883                    }
884
885                    if (++mEmptyCursorCount > RETRIES_ON_NO_DATA) {
886                        // Nothing in the cursor again. Dropping query
887                        mQueryQueue.poll();
888                    }
889                }
890
891                updateHeaderFooter(totalAgendaRangeStart, totalAgendaRangeEnd);
892
893                // Fire off the next query if any
894                Iterator<QuerySpec> it = mQueryQueue.iterator();
895                while (it.hasNext()) {
896                    QuerySpec queryData = it.next();
897                    if (!isInRange(queryData.start, queryData.end)) {
898                        // Query accepted
899                        if (DEBUGLOG) Log.e(TAG, "Query accepted. QueueSize:" + mQueryQueue.size());
900                        doQuery(queryData);
901                        break;
902                    } else {
903                        // Query rejected
904                        it.remove();
905                        if (DEBUGLOG) Log.e(TAG, "Query rejected. QueueSize:" + mQueryQueue.size());
906                    }
907                }
908            }
909            if (BASICLOG) {
910                for (DayAdapterInfo info3 : mAdapterInfos) {
911                    Log.e(TAG, "> " + info3.toString());
912                }
913            }
914        }
915
916        /*
917         * Update the adapter info array with a the new cursor. Close out old
918         * cursors as needed.
919         *
920         * @return number of rows removed from the beginning
921         */
922        private int processNewCursor(QuerySpec data, Cursor cursor) {
923            synchronized (mAdapterInfos) {
924                // Remove adapter info's from adapterInfos as needed
925                DayAdapterInfo info = pruneAdapterInfo(data.queryType);
926                int listPositionOffset = 0;
927                if (info == null) {
928                    info = new DayAdapterInfo(mContext);
929                } else {
930                    if (DEBUGLOG)
931                        Log.e(TAG, "processNewCursor listPositionOffsetA="
932                                + -info.size);
933                    listPositionOffset = -info.size;
934                }
935
936                // Setup adapter info
937                info.start = data.start;
938                info.end = data.end;
939                info.cursor = cursor;
940                info.dayAdapter.changeCursor(info);
941                info.size = info.dayAdapter.getCount();
942
943                // Insert into adapterInfos
944                if (mAdapterInfos.isEmpty()
945                        || data.end <= mAdapterInfos.getFirst().start) {
946                    mAdapterInfos.addFirst(info);
947                    listPositionOffset += info.size;
948                } else if (BASICLOG && data.start < mAdapterInfos.getLast().end) {
949                    mAdapterInfos.addLast(info);
950                    for (DayAdapterInfo info2 : mAdapterInfos) {
951                        Log.e("========== BUG ==", info2.toString());
952                    }
953                } else {
954                    mAdapterInfos.addLast(info);
955                }
956
957                // Update offsets in adapterInfos
958                mRowCount = 0;
959                for (DayAdapterInfo info3 : mAdapterInfos) {
960                    info3.offset = mRowCount;
961                    mRowCount += info3.size;
962                }
963                mLastUsedInfo = null;
964
965                return listPositionOffset;
966            }
967        }
968    }
969
970    static String getViewTitle(View x) {
971        String title = "";
972        if (x != null) {
973            Object yy = x.getTag();
974            if (yy instanceof AgendaAdapter.ViewHolder) {
975                TextView tv = ((AgendaAdapter.ViewHolder) yy).title;
976                if (tv != null) {
977                    title = (String) tv.getText();
978                }
979            } else if (yy != null) {
980                TextView dateView = ((AgendaByDayAdapter.ViewHolder) yy).dateView;
981                if (dateView != null) {
982                    title = (String) dateView.getText();
983                }
984            }
985        }
986        return title;
987    }
988
989    public void onResume() {
990        mTZUpdater.run();
991    }
992
993    public void setHideDeclinedEvents(boolean hideDeclined) {
994        mHideDeclined = hideDeclined;
995    }
996
997    public void setSelectedView(View v) {
998        if (v != null) {
999            Object vh = v.getTag();
1000            if (vh instanceof AgendaAdapter.ViewHolder) {
1001                mSelectedInstanceId = ((AgendaAdapter.ViewHolder) vh).instanceId;
1002            }
1003        }
1004    }
1005
1006    public long getSelectedInstanceId() {
1007        return mSelectedInstanceId;
1008    }
1009
1010    public void setSelectedInstanceId(long selectedInstanceId) {
1011        mSelectedInstanceId = selectedInstanceId;
1012    }
1013
1014
1015    // Implementation of HeaderIndexer interface for StickyHeeaderListView
1016
1017    // Returns the location of the day header of a specific event specified in the position
1018    // in the adapter
1019    public int getHeaderPositionFromItemPosition(int position) {
1020        DayAdapterInfo info = getAdapterInfoByPosition(position);
1021        if (info != null) {
1022            int pos = info.dayAdapter.getHeaderPosition(position - info.offset);
1023            return (pos != -1)?(pos + info.offset):-1;
1024        }
1025        return -1;
1026    }
1027
1028    // Returns the number of events for a specific day header
1029    public int getHeaderItemsNumber(int headerPosition) {
1030        if (headerPosition < 0) {
1031            return -1;
1032        }
1033        DayAdapterInfo info = getAdapterInfoByPosition(headerPosition);
1034        if (info != null) {
1035            return info.dayAdapter.getHeaderItemsCount(headerPosition - info.offset);
1036        }
1037        return -1;
1038    }
1039}
1040