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