Modified Agenda view to support scrolling beyond the month boundary.

	new file:   res/layout/agenda_header_footer.xml
	modified:   res/values/strings.xml
	modified:   src/com/android/calendar/AgendaActivity.java
	modified:   src/com/android/calendar/AgendaAdapter.java
	modified:   src/com/android/calendar/AgendaByDayAdapter.java
	new file:   src/com/android/calendar/AgendaListView.java
	new file:   src/com/android/calendar/AgendaWindowAdapter.java
diff --git a/res/layout/agenda_header_footer.xml b/res/layout/agenda_header_footer.xml
new file mode 100644
index 0000000..96824f5
--- /dev/null
+++ b/res/layout/agenda_header_footer.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginLeft="10dip"
+    android:gravity="center_vertical"
+    android:textColor="?android:attr/textColorSecondary"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingLeft="10dip"
+    android:paddingRight="10dip"
+    style="?android:attr/textAppearanceMediumInverse" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index e49015e..c463ef6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -230,6 +230,12 @@
     <skip />
     <!-- This is shown as part of the heading at the top of a list of today's events. -->
     <string name="agenda_today">Today</string>
+    <!-- This is shown while the calendar events are being loading to the screen. -->
+    <string name="loading">Loading\u2026</string>
+    <!-- This is shown at the top of the agenda view showing the range of events shown. -->
+    <string name="show_older_events">Showing events since <xliff:g id="oldest_search_range">%1$s</xliff:g>. Tap to look for more.</string>
+    <!-- This is shown at the bottom of the agenda view showing the range of events shown. -->
+    <string name="show_newer_events">Showing events until <xliff:g id="newest_search_range">%1$s</xliff:g>. Tap to look for more.</string>
     
     <!-- ICS Import activity -->
     <skip />
diff --git a/src/com/android/calendar/AgendaActivity.java b/src/com/android/calendar/AgendaActivity.java
index 9f5a53e..15c9387 100644
--- a/src/com/android/calendar/AgendaActivity.java
+++ b/src/com/android/calendar/AgendaActivity.java
@@ -16,162 +16,43 @@
 
 package com.android.calendar;
 
+import static android.provider.Calendar.EVENT_BEGIN_TIME;
+import dalvik.system.VMRuntime;
+
 import android.app.Activity;
-import android.content.AsyncQueryHandler;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
-import android.content.ContentUris;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
 import android.database.ContentObserver;
-import android.database.Cursor;
-import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.preference.PreferenceManager;
-import android.provider.Calendar;
-import android.provider.Calendar.Attendees;
-import android.provider.Calendar.Calendars;
 import android.provider.Calendar.Events;
-import android.provider.Calendar.Instances;
 import android.text.format.Time;
+import android.util.Log;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
-import android.view.View;
-import android.view.Window;
-import android.widget.AdapterView;
-import android.widget.ListView;
-import android.widget.ViewSwitcher;
-import dalvik.system.VMRuntime;
 
-public class AgendaActivity extends Activity implements ViewSwitcher.ViewFactory, Navigator {
+public class AgendaActivity extends Activity implements Navigator {
+
+    private static final String TAG = "AgendaActivity";
+
+    private static boolean DEBUG = false;
 
     protected static final String BUNDLE_KEY_RESTORE_TIME = "key_restore_time";
 
-    static final String[] PROJECTION = new String[] {
-        Instances._ID,                  // 0
-        Instances.TITLE,                // 1
-        Instances.EVENT_LOCATION,       // 2
-        Instances.ALL_DAY,              // 3
-        Instances.HAS_ALARM,            // 4
-        Instances.COLOR,                // 5
-        Instances.RRULE,                // 6
-        Instances.BEGIN,                // 7
-        Instances.END,                  // 8
-        Instances.EVENT_ID,             // 9
-        Instances.START_DAY,            // 10  Julian start day
-        Instances.END_DAY,              // 11  Julian end day
-        Instances.SELF_ATTENDEE_STATUS, // 12
-    };
-
-    public static final int INDEX_TITLE = 1;
-    public static final int INDEX_EVENT_LOCATION = 2;
-    public static final int INDEX_ALL_DAY = 3;
-    public static final int INDEX_HAS_ALARM = 4;
-    public static final int INDEX_COLOR = 5;
-    public static final int INDEX_RRULE = 6;
-    public static final int INDEX_BEGIN = 7;
-    public static final int INDEX_END = 8;
-    public static final int INDEX_EVENT_ID = 9;
-    public static final int INDEX_START_DAY = 10;
-    public static final int INDEX_END_DAY = 11;
-    public static final int INDEX_SELF_ATTENDEE_STATUS = 12;
-
-    public static final String AGENDA_SORT_ORDER = "startDay ASC, begin ASC, title ASC";
-
     private static final long INITIAL_HEAP_SIZE = 4*1024*1024;
 
     private ContentResolver mContentResolver;
 
-    private ViewSwitcher mViewSwitcher;
+    private AgendaListView mAgendaListView;
 
-    private QueryHandler mQueryHandler;
-    private DeleteEventHelper mDeleteEventHelper;
     private Time mTime;
 
-    /**
-     * This records the start time parameter for the last query sent to the
-     * AsyncQueryHandler so that we don't send it duplicate query requests.
-     */
-    private Time mLastQueryTime = new Time();
-
-    private class QueryHandler extends AsyncQueryHandler {
-        public QueryHandler(ContentResolver cr) {
-            super(cr);
-        }
-
-        @Override
-        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
-
-            // Only set mCursor if the Activity is not finishing. Otherwise close the cursor.
-            if (!isFinishing()) {
-                AgendaListView next = (AgendaListView) mViewSwitcher.getNextView();
-                next.setCursor(cursor);
-                mViewSwitcher.showNext();
-                selectTime();
-            } else {
-                cursor.close();
-            }
-        }
-    }
-
-    private class AgendaListView extends ListView {
-        private Cursor mCursor;
-        private AgendaByDayAdapter mDayAdapter;
-        private AgendaAdapter mAdapter;
-
-        public AgendaListView(Context context) {
-            super(context, null);
-            setOnItemClickListener(mOnItemClickListener);
-            setChoiceMode(ListView.CHOICE_MODE_SINGLE);
-            mAdapter = new AgendaAdapter(AgendaActivity.this, R.layout.agenda_item);
-            mDayAdapter = new AgendaByDayAdapter(AgendaActivity.this, mAdapter);
-        }
-
-        public void setCursor(Cursor cursor) {
-            if (mCursor != null) {
-                mCursor.close();
-            }
-            mCursor = cursor;
-            mDayAdapter.calculateDays(cursor);
-            mAdapter.changeCursor(cursor);
-            setAdapter(mDayAdapter);
-        }
-
-        public Cursor getCursor() {
-            return mCursor;
-        }
-
-        public AgendaByDayAdapter getDayAdapter() {
-            return mDayAdapter;
-        }
-
-        @Override protected void onDetachedFromWindow() {
-            super.onDetachedFromWindow();
-            if (mCursor != null) {
-                mCursor.close();
-            }
-        }
-
-        private OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
-            public void onItemClick(AdapterView a, View v, int position, long id) {
-                if (id != -1) {
-                    // Switch to the EventInfo view
-                    mCursor.moveToPosition(mDayAdapter.getCursorPosition(position));
-                    long eventId = mCursor.getLong(INDEX_EVENT_ID);
-                    Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
-                    Intent intent = new Intent(Intent.ACTION_VIEW, uri);
-                    intent.putExtra(Calendar.EVENT_BEGIN_TIME, mCursor.getLong(INDEX_BEGIN));
-                    intent.putExtra(Calendar.EVENT_END_TIME, mCursor.getLong(INDEX_END));
-                    startActivity(intent);
-                }
-            }
-        };
-    }
-
     private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -179,8 +60,7 @@
             if (action.equals(Intent.ACTION_TIME_CHANGED)
                     || action.equals(Intent.ACTION_DATE_CHANGED)
                     || action.equals(Intent.ACTION_TIMEZONE_CHANGED)) {
-                clearLastQueryTime();
-                renewCursor();
+                mAgendaListView.refresh(true);
             }
         }
     };
@@ -193,8 +73,7 @@
 
         @Override
         public void onChange(boolean selfChange) {
-            clearLastQueryTime();
-            renewCursor();
+            mAgendaListView.refresh(true);
         }
     };
 
@@ -206,26 +85,42 @@
         // TODO: We should restore the old heap size once the activity reaches the idle state
         VMRuntime.getRuntime().setMinimumHeapSize(INITIAL_HEAP_SIZE);
 
-        setContentView(R.layout.agenda_activity);
+        mAgendaListView = new AgendaListView(this);
+        setContentView(mAgendaListView);
 
         mContentResolver = getContentResolver();
-        mQueryHandler = new QueryHandler(mContentResolver);
 
-        // Preserve the same month and event selection if this activity is
-        // being restored due to an orientation change
-        mTime = new Time();
-        if (icicle != null) {
-            mTime.set(icicle.getLong(BUNDLE_KEY_RESTORE_TIME));
-        } else {
-            mTime.set(Utils.timeFromIntent(getIntent()));
-        }
         setTitle(R.string.agenda_view);
 
-        mViewSwitcher = (ViewSwitcher) findViewById(R.id.switcher);
-        mViewSwitcher.setFactory(this);
+        long millis = 0;
+        mTime = new Time();
+        if (icicle != null) {
+            // Returns 0 if key not found
+            millis = icicle.getLong(BUNDLE_KEY_RESTORE_TIME);
+            if (DEBUG) {
+                Log.v(TAG, "Restore value from icicle: " + millis);
+            }
+        }
+
+        if (millis == 0) {
+            // Returns 0 if key not found
+            millis = getIntent().getLongExtra(EVENT_BEGIN_TIME, 0);
+            if (DEBUG) {
+                Log.v(TAG, "Restore value from intent: " + millis);
+            }
+        }
+
+        if (millis == 0) {
+            if (DEBUG) {
+                Log.v(TAG, "Restored from current time");
+            }
+            millis = System.currentTimeMillis();
+        }
+        mTime.set(millis);
 
         // Record Agenda View as the (new) default detailed view.
-        String activityString = CalendarApplication.ACTIVITY_NAMES[CalendarApplication.AGENDA_VIEW_ID];
+        String activityString =
+            CalendarApplication.ACTIVITY_NAMES[CalendarApplication.AGENDA_VIEW_ID];
         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
         SharedPreferences.Editor editor = prefs.edit();
         editor.putString(CalendarPreferenceActivity.KEY_DETAILED_VIEW, activityString);
@@ -233,16 +128,22 @@
         // Record Agenda View as the (new) start view
         editor.putString(CalendarPreferenceActivity.KEY_START_VIEW, activityString);
         editor.commit();
-
-        mDeleteEventHelper = new DeleteEventHelper(this, false /* don't exit when done */);
     }
 
     @Override
     protected void onResume() {
         super.onResume();
+        if (DEBUG) {
+            Log.v(TAG, "OnResume to " + mTime.toString());
+        }
 
-        clearLastQueryTime();
-        renewCursor();
+        SharedPreferences prefs =
+            PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+        boolean hideDeclined = prefs
+                .getBoolean(CalendarPreferenceActivity.KEY_HIDE_DECLINED, false);
+
+        mAgendaListView.setHideDeclinedEvents(hideDeclined);
+        mAgendaListView.goTo(mTime, true);
 
         // Register for Intent broadcasts
         IntentFilter filter = new IntentFilter();
@@ -258,7 +159,14 @@
     protected void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
 
-        outState.putLong(BUNDLE_KEY_RESTORE_TIME, getSelectedTime());
+        long firstVisibleTime = mAgendaListView.getFirstVisibleTime();
+        if (firstVisibleTime >= 0) {
+            mTime.set(firstVisibleTime);
+            outState.putLong(BUNDLE_KEY_RESTORE_TIME, firstVisibleTime);
+            if (DEBUG) {
+                Log.v(TAG, "onSaveInstanceState " + mTime.toString());
+            }
+        }
     }
 
     @Override
@@ -290,22 +198,9 @@
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         switch (keyCode) {
-            case KeyEvent.KEYCODE_DEL: {
+            case KeyEvent.KEYCODE_DEL:
                 // Delete the currently selected event (if any)
-                AgendaListView current = (AgendaListView) mViewSwitcher.getCurrentView();
-                Cursor cursor = current.getCursor();
-                if (cursor != null) {
-                    int position = current.getSelectedItemPosition();
-                    position = current.getDayAdapter().getCursorPosition(position);
-                    if (position >= 0) {
-                        cursor.moveToPosition(position);
-                        long begin = cursor.getLong(INDEX_BEGIN);
-                        long end = cursor.getLong(INDEX_END);
-                        long eventId = cursor.getLong(INDEX_EVENT_ID);
-                        mDeleteEventHelper.delete(begin, end, eventId, -1);
-                    }
-                }
-            }
+                mAgendaListView.deleteSelectedEvent();
                 break;
 
             case KeyEvent.KEYCODE_BACK:
@@ -315,81 +210,6 @@
         return super.onKeyDown(keyCode, event);
     }
 
-    /**
-     * Clears the cached value for the last query time so that renewCursor()
-     * will force a requery of the Calendar events.
-     */
-    private void clearLastQueryTime() {
-        mLastQueryTime.year = 0;
-        mLastQueryTime.month = 0;
-    }
-
-    private void renewCursor() {
-        // Avoid batching up repeated queries for the same month.  This can
-        // happen if the user scrolls with the trackball too fast.
-        if (mLastQueryTime.month == mTime.month && mLastQueryTime.year == mTime.year) {
-            return;
-        }
-
-        // Query all instances for the current month
-        Time time = new Time();
-        time.year = mTime.year;
-        time.month = mTime.month;
-        long start = time.normalize(true);
-
-        time.month++;
-        long end = time.normalize(true);
-
-        StringBuilder path = new StringBuilder();
-        path.append(start);
-        path.append('/');
-        path.append(end);
-
-        // Respect the preference to show/hide declined events
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
-        boolean hideDeclined = prefs.getBoolean(CalendarPreferenceActivity.KEY_HIDE_DECLINED,
-                false);
-
-        Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI, path.toString());
-
-        String selection;
-        if (hideDeclined) {
-            selection = Calendars.SELECTED + "=1 AND " +
-                    Instances.SELF_ATTENDEE_STATUS + "!=" + Attendees.ATTENDEE_STATUS_DECLINED;
-        } else {
-            selection = Calendars.SELECTED + "=1";
-        }
-
-        // Cancel any previous queries that haven't started yet.  This
-        // isn't likely to happen since we already avoid sending
-        // a duplicate query for the same month as the previous query.
-        // But if the user quickly wiggles the trackball back and forth,
-        // he could generate a stream of queries.
-        mQueryHandler.cancelOperation(0);
-
-        mLastQueryTime.month = mTime.month;
-        mLastQueryTime.year = mTime.year;
-        mQueryHandler.startQuery(0, null, uri, PROJECTION, selection, null,
-                AGENDA_SORT_ORDER);
-    }
-
-    private void selectTime() {
-        // Selects the first event of the day
-        AgendaListView current = (AgendaListView) mViewSwitcher.getCurrentView();
-        if (current.getCursor() == null) {
-            return;
-        }
-
-        int position = current.getDayAdapter().findDayPositionNearestTime(mTime);
-        current.setSelection(position);
-    }
-
-    /* ViewSwitcher.ViewFactory interface methods */
-    public View makeView() {
-        AgendaListView agendaListView = new AgendaListView(this);
-        return agendaListView;
-    }
-
     /* Navigator interface methods */
     public void goToToday() {
         Time now = new Time();
@@ -398,27 +218,11 @@
     }
 
     public void goTo(Time time) {
-        if (mTime.year == time.year && mTime.month == time.month) {
-            mTime = time;
-            selectTime();
-        } else {
-            mTime = time;
-            renewCursor();
-        }
+        mAgendaListView.goTo(time, false);
     }
 
     public long getSelectedTime() {
-        // Update the current time based on the selected event
-        AgendaListView current = (AgendaListView) mViewSwitcher.getCurrentView();
-        int position = current.getSelectedItemPosition();
-        position = current.getDayAdapter().getCursorPosition(position);
-        Cursor cursor = current.getCursor();
-        if (position >= 0 && position < cursor.getCount()) {
-            cursor.moveToPosition(position);
-            mTime.set(cursor.getLong(INDEX_BEGIN));
-        }
-
-        return mTime.toMillis(true);
+        return mAgendaListView.getSelectedTime();
     }
 
     public boolean getAllDay() {
diff --git a/src/com/android/calendar/AgendaAdapter.java b/src/com/android/calendar/AgendaAdapter.java
index 69649ea..8603fc7 100644
--- a/src/com/android/calendar/AgendaAdapter.java
+++ b/src/com/android/calendar/AgendaAdapter.java
@@ -26,10 +26,15 @@
 import android.widget.ResourceCursorAdapter;
 import android.widget.TextView;
 
+import java.util.Formatter;
+import java.util.Locale;
+
 public class AgendaAdapter extends ResourceCursorAdapter {
     private String mNoTitleLabel;
     private Resources mResources;
     private int mDeclinedColor;
+    private Formatter mFormatter; // TODO fix. not thread safe
+    private StringBuilder mStringBuilder;
 
     static class ViewHolder {
         int overLayColor; // Used by AgendaItemView to gray out the entire item if so desired
@@ -46,11 +51,20 @@
         mResources = context.getResources();
         mNoTitleLabel = mResources.getString(R.string.no_title_label);
         mDeclinedColor = mResources.getColor(R.drawable.agenda_item_declined);
+        mStringBuilder = new StringBuilder(50);
+        mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
     }
 
     @Override
     public void bindView(View view, Context context, Cursor cursor) {
-        ViewHolder holder = (ViewHolder) view.getTag();
+        ViewHolder holder = null;
+
+        // Listview may get confused and pass in a different type of view since
+        // we keep shifting data around. Not a big problem.
+        Object tag = view.getTag();
+        if (tag instanceof ViewHolder) {
+            holder = (ViewHolder) view.getTag();
+        }
 
         if (holder == null) {
             holder = new ViewHolder();
@@ -61,7 +75,7 @@
         }
 
         // Fade text if event was declined.
-        int selfAttendeeStatus = cursor.getInt(AgendaActivity.INDEX_SELF_ATTENDEE_STATUS);
+        int selfAttendeeStatus = cursor.getInt(AgendaWindowAdapter.INDEX_SELF_ATTENDEE_STATUS);
         if (selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED) {
             holder.overLayColor = mDeclinedColor;
         } else {
@@ -73,11 +87,11 @@
         TextView where = holder.where;
 
         /* Calendar Color */
-        int color = cursor.getInt(AgendaActivity.INDEX_COLOR);
+        int color = cursor.getInt(AgendaWindowAdapter.INDEX_COLOR);
         holder.calendarColor = color;
 
         // What
-        String titleString = cursor.getString(AgendaActivity.INDEX_TITLE);
+        String titleString = cursor.getString(AgendaWindowAdapter.INDEX_TITLE);
         if (titleString == null || titleString.length() == 0) {
             titleString = mNoTitleLabel;
         }
@@ -85,9 +99,9 @@
         title.setTextColor(color);
 
         // When
-        long begin = cursor.getLong(AgendaActivity.INDEX_BEGIN);
-        long end = cursor.getLong(AgendaActivity.INDEX_END);
-        boolean allDay = cursor.getInt(AgendaActivity.INDEX_ALL_DAY) != 0;
+        long begin = cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN);
+        long end = cursor.getLong(AgendaWindowAdapter.INDEX_END);
+        boolean allDay = cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0;
         int flags;
         String whenString;
         if (allDay) {
@@ -98,10 +112,11 @@
         if (DateFormat.is24HourFormat(context)) {
             flags |= DateUtils.FORMAT_24HOUR;
         }
-        whenString = DateUtils.formatDateRange(context, begin, end, flags);
+        mStringBuilder.setLength(0);
+        whenString = DateUtils.formatDateRange(context, mFormatter, begin, end, flags).toString();
         when.setText(whenString);
 
-        String rrule = cursor.getString(AgendaActivity.INDEX_RRULE);
+        String rrule = cursor.getString(AgendaWindowAdapter.INDEX_RRULE);
         if (rrule != null) {
             when.setCompoundDrawablesWithIntrinsicBounds(null, null,
                     context.getResources().getDrawable(R.drawable.ic_repeat_dark), null);
@@ -130,7 +145,7 @@
         */
 
         // Where
-        String whereString = cursor.getString(AgendaActivity.INDEX_EVENT_LOCATION);
+        String whereString = cursor.getString(AgendaWindowAdapter.INDEX_EVENT_LOCATION);
         if (whereString != null && whereString.length() > 0) {
             where.setVisibility(View.VISIBLE);
             where.setText(whereString);
diff --git a/src/com/android/calendar/AgendaByDayAdapter.java b/src/com/android/calendar/AgendaByDayAdapter.java
index 140eb72..5d26e3d 100644
--- a/src/com/android/calendar/AgendaByDayAdapter.java
+++ b/src/com/android/calendar/AgendaByDayAdapter.java
@@ -27,26 +27,35 @@
 import android.widget.TextView;
 
 import java.util.ArrayList;
-import java.util.Calendar;
+import java.util.Formatter;
 import java.util.Iterator;
 import java.util.LinkedList;
+import java.util.Locale;
 
 public class AgendaByDayAdapter extends BaseAdapter {
     private static final int TYPE_DAY = 0;
     private static final int TYPE_MEETING = 1;
-    private static final int TYPE_LAST = 2;
+    static final int TYPE_LAST = 2;
 
     private final Context mContext;
     private final AgendaAdapter mAgendaAdapter;
     private final LayoutInflater mInflater;
     private ArrayList<RowInfo> mRowInfo;
     private int mTodayJulianDay;
-    private Time mTime = new Time();
+    private Time mTmpTime = new Time();
+    private Formatter mFormatter; // TODO fix. not thread safe
+    private StringBuilder mStringBuilder;
 
-    public AgendaByDayAdapter(Context context, AgendaAdapter agendaAdapter) {
+    static class ViewHolder {
+        TextView dateView;
+    }
+
+    public AgendaByDayAdapter(Context context) {
         mContext = context;
-        mAgendaAdapter = agendaAdapter;
-        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mAgendaAdapter = new AgendaAdapter(context, R.layout.agenda_item);
+        mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mStringBuilder = new StringBuilder(50);
+        mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
     }
 
     public int getCount() {
@@ -91,10 +100,6 @@
                 mRowInfo.get(position).mType : TYPE_DAY;
     }
 
-    private static class ViewHolder {
-        TextView dateView;
-    }
-
     public View getView(int position, View convertView, ViewGroup parent) {
         if ((mRowInfo == null) || (position > mRowInfo.size())) {
             // If we have no row info, mAgendaAdapter returns the view.
@@ -103,38 +108,51 @@
 
         RowInfo row = mRowInfo.get(position);
         if (row.mType == TYPE_DAY) {
-            ViewHolder holder;
-            View agendaDayView;
-            if ((convertView == null) || (convertView.getTag() == null)) {
+            ViewHolder holder = null;
+            View agendaDayView = null;
+            if ((convertView != null) && (convertView.getTag() != null)) {
+                // Listview may get confused and pass in a different type of
+                // view since we keep shifting data around. Not a big problem.
+                Object tag = convertView.getTag();
+                if (tag instanceof ViewHolder) {
+                    agendaDayView = convertView;
+                    holder = (ViewHolder) tag;
+                }
+            }
+
+            if (holder == null) {
                 // Create a new AgendaView with a ViewHolder for fast access to
                 // views w/o calling findViewById()
                 holder = new ViewHolder();
                 agendaDayView = mInflater.inflate(R.layout.agenda_day, parent, false);
                 holder.dateView = (TextView) agendaDayView.findViewById(R.id.date);
                 agendaDayView.setTag(holder);
-            } else {
-                agendaDayView = convertView;
-                holder = (ViewHolder) convertView.getTag();
             }
 
             // Re-use the member variable "mTime" which is set to the local timezone.
-            Time date = mTime;
+            Time date = mTmpTime;
             long millis = date.setJulianDay(row.mData);
             int flags = DateUtils.FORMAT_SHOW_YEAR
                     | DateUtils.FORMAT_SHOW_DATE;
-            
+
+            mStringBuilder.setLength(0);
             if (row.mData == mTodayJulianDay) {
                 String dayText = mContext.getResources().getText(R.string.agenda_today) + ", ";
-                holder.dateView.setText(dayText + DateUtils.formatDateTime(mContext, millis, flags));
+                holder.dateView.setText(dayText + DateUtils.formatDateRange(mContext, mFormatter,
+                    millis, millis, flags).toString() + " P:" + position); // TODO remove P:
             } else {
                 flags |= DateUtils.FORMAT_SHOW_WEEKDAY;
-                holder.dateView.setText(DateUtils.formatDateTime(mContext, millis, flags));
+                holder.dateView.setText(DateUtils.formatDateRange(mContext, mFormatter, millis,
+                    millis, flags).toString() + " P:" + position); // TODO remove P:
             }
 
 
             return agendaDayView;
         } else if (row.mType == TYPE_MEETING) {
-            return mAgendaAdapter.getView(row.mData, convertView, parent);
+            View x = mAgendaAdapter.getView(row.mData, convertView, parent);
+            TextView y = ((AgendaAdapter.ViewHolder) x.getTag()).title;
+            y.setText(y.getText() + " P:" + position);
+            return x;
         } else {
             // Error
             throw new IllegalStateException("Unknown event type:" + row.mType);
@@ -145,6 +163,11 @@
         mRowInfo = null;
     }
 
+    public void changeCursor(Cursor cursor) {
+        calculateDays(cursor);
+        mAgendaAdapter.changeCursor(cursor);
+    }
+
     public void calculateDays(Cursor cursor) {
         ArrayList<RowInfo> rowInfo = new ArrayList<RowInfo>();
         int prevStartDay = -1;
@@ -154,8 +177,8 @@
         mTodayJulianDay = Time.getJulianDay(now, time.gmtoff);
         LinkedList<MultipleDayInfo> multipleDayList = new LinkedList<MultipleDayInfo>();
         for (int position = 0; cursor.moveToNext(); position++) {
-            boolean allDay = cursor.getInt(AgendaActivity.INDEX_ALL_DAY) != 0;
-            int startDay = cursor.getInt(AgendaActivity.INDEX_START_DAY);
+            boolean allDay = cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0;
+            int startDay = cursor.getInt(AgendaWindowAdapter.INDEX_START_DAY);
 
             if (startDay != prevStartDay) {
                 // Check if we skipped over any empty days
@@ -202,7 +225,7 @@
 
             // If this event spans multiple days, then add it to the multipleDay
             // list.
-            int endDay = cursor.getInt(AgendaActivity.INDEX_END_DAY);
+            int endDay = cursor.getInt(AgendaWindowAdapter.INDEX_END_DAY);
             if (endDay > startDay) {
                 multipleDayList.add(new MultipleDayInfo(position, endDay));
             }
@@ -215,7 +238,7 @@
             // we set the date to one less than the first day of the next month,
             // and then normalize.
             time.setJulianDay(prevStartDay);
-            time.month += 1;
+            time.month += 1;  // TODO remove month query reference
             time.monthDay = 0;  // monthDay starts with 1, so this is the previous day
             long millis = time.normalize(true /* ignore isDst */);
             int lastDayOfMonth = Time.getJulianDay(millis, time.gmtoff);
@@ -342,9 +365,17 @@
             RowInfo row = mRowInfo.get(listPos);
             if (row.mType == TYPE_MEETING) {
                 return row.mData;
+            } else {
+                int nextPos = listPos + 1;
+                if (nextPos < mRowInfo.size()) {
+                    nextPos = getCursorPosition(nextPos);
+                    if (nextPos >= 0) {
+                        return -nextPos;
+                    }
+                }
             }
         }
-        return listPos;
+        return Integer.MIN_VALUE;
     }
 
     @Override
@@ -361,4 +392,3 @@
         return true;
     }
 }
-
diff --git a/src/com/android/calendar/AgendaListView.java b/src/com/android/calendar/AgendaListView.java
new file mode 100644
index 0000000..09bd7ed
--- /dev/null
+++ b/src/com/android/calendar/AgendaListView.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.calendar;
+
+import com.android.calendar.AgendaAdapter.ViewHolder;
+import com.android.calendar.AgendaWindowAdapter.EventInfo;
+
+import android.content.ContentUris;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.provider.Calendar;
+import android.provider.Calendar.Events;
+import android.text.format.Time;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.AdapterView.OnItemClickListener;
+
+public class AgendaListView extends ListView implements OnItemClickListener {
+
+    private static final String TAG = "AgendaListView";
+    private static final boolean DEBUG = false;
+
+    private AgendaWindowAdapter mWindowAdapter;
+
+    private AgendaActivity mAgendaActivity;
+    private DeleteEventHelper mDeleteEventHelper;
+
+    public AgendaListView(AgendaActivity agendaActivity) {
+        super(agendaActivity, null);
+        mAgendaActivity = agendaActivity;
+        mContext = agendaActivity;
+
+        setOnItemClickListener(this);
+        setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+        mWindowAdapter = new AgendaWindowAdapter(agendaActivity, this);
+        setAdapter(mWindowAdapter);
+        mDeleteEventHelper =
+            new DeleteEventHelper(agendaActivity, false /* don't exit when done */);
+    }
+
+    @Override protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mWindowAdapter.close();
+    }
+
+    // Implementation of the interface OnItemClickListener
+    public void onItemClick(AdapterView<?> a, View v, int position, long id) {
+        if (id != -1) {
+            // Switch to the EventInfo view
+            EventInfo event = mWindowAdapter.getEventByPosition(position);
+            if (event != null) {
+                Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, event.id);
+                Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+                intent.putExtra(Calendar.EVENT_BEGIN_TIME, event.begin);
+                intent.putExtra(Calendar.EVENT_END_TIME, event.end);
+                mAgendaActivity.startActivity(intent);
+            }
+        }
+    }
+
+    public void goTo(Time time, boolean forced) {
+        mWindowAdapter.refresh(time, forced);
+    }
+
+    public void refresh(boolean forced) {
+        Time time = new Time();
+        time.set(getFirstVisiblePosition());
+        mWindowAdapter.refresh(time, forced);
+    }
+
+    public void deleteSelectedEvent() {
+        int position = getSelectedItemPosition();
+        EventInfo event = mWindowAdapter.getEventByPosition(position);
+        if (event != null) {
+            mDeleteEventHelper.delete(event.begin, event.end, event.id, -1);
+        }
+    }
+
+    @Override
+    public int getFirstVisiblePosition() {
+        // TODO File bug!
+        // getFirstVisiblePosition doesn't always return the first visible
+        // item. Sometimes, it is above the visible one.
+        // instead. I loop through the viewgroup children and find the first
+        // visible one. BTW, getFirstVisiblePosition() == getChildAt(0). I
+        // am not looping through the entire list.
+       View v = getFirstVisibleView();
+       if (v != null) {
+           if (DEBUG) {
+               Log.v(TAG, "getFirstVisiblePosition: " + AgendaWindowAdapter.getViewTitle(v));
+           }
+           return getPositionForView(v);
+       }
+       return -1;
+    }
+
+    public View getFirstVisibleView() {
+        Rect r = new Rect();
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; ++i) {
+            View listItem = getChildAt(i);
+            listItem.getLocalVisibleRect(r);
+            if (r.top >= 0) { // if visible
+                return listItem;
+            }
+        }
+        return null;
+    }
+
+    public long getSelectedTime() {
+        int position = getSelectedItemPosition();
+
+        EventInfo event = mWindowAdapter.getEventByPosition(position);
+        if (event != null) {
+            return event.begin;
+        }
+        return -1;
+    }
+
+    public long getFirstVisibleTime() {
+        int position = getFirstVisiblePosition();
+        if (DEBUG) {
+            Log.v(TAG, "getFirstVisiblePosition = " + position);
+        }
+
+        EventInfo event = mWindowAdapter.getEventByPosition(position);
+        if (event != null) {
+            return event.begin;
+        }
+        return -1;
+    }
+
+    // Move the currently selected or visible focus down by offset amount.
+    // offset could be negative.
+    public void shiftSelection(int offset) {
+        shiftPosition(offset);
+        int position = getSelectedItemPosition();
+        if (position != INVALID_POSITION) {
+            setSelectionFromTop(position + offset, 0);
+        }
+    }
+
+    private void shiftPosition(int offset) {
+        if (DEBUG) {
+            Log.v(TAG, "Shifting position "+ offset);
+        }
+
+        View firstVisibleItem = getFirstVisibleView();
+
+        if (firstVisibleItem != null) {
+            Rect r = new Rect();
+            firstVisibleItem.getLocalVisibleRect(r);
+            // if r.top is < 0, getChildAt(0) and getFirstVisiblePosition() is
+            // returning an item above the first visible item.
+            int position = getPositionForView(firstVisibleItem);
+            setSelectionFromTop(position + offset, r.top > 0 ? -r.top : r.top);
+            if (DEBUG) {
+                if (firstVisibleItem.getTag() instanceof AgendaAdapter.ViewHolder) {
+                    ViewHolder viewHolder = (AgendaAdapter.ViewHolder)firstVisibleItem.getTag();
+                    Log.v(TAG, "Shifting from " + position + " by " + offset + ". Title "
+                            + viewHolder.title.getText());
+                } else if (firstVisibleItem.getTag() instanceof AgendaByDayAdapter.ViewHolder) {
+                    AgendaByDayAdapter.ViewHolder viewHolder =
+                        (AgendaByDayAdapter.ViewHolder)firstVisibleItem.getTag();
+                    Log.v(TAG, "Shifting from " + position + " by " + offset + ". Date  "
+                            + viewHolder.dateView.getText());
+                } else if (firstVisibleItem instanceof TextView) {
+                    Log.v(TAG, "Shifting: Looking at header here. " + getSelectedItemPosition());
+                }
+            }
+        } else if (getSelectedItemPosition() >= 0) {
+            if (DEBUG) {
+                Log.v(TAG, "Shifting selection from " + getSelectedItemPosition() + " by " + offset);
+            }
+            setSelection(getSelectedItemPosition() + offset);
+        }
+    }
+
+    public void setHideDeclinedEvents(boolean hideDeclined) {
+        mWindowAdapter.setHideDeclinedEvents(hideDeclined);
+    }
+}
diff --git a/src/com/android/calendar/AgendaWindowAdapter.java b/src/com/android/calendar/AgendaWindowAdapter.java
new file mode 100644
index 0000000..cede190
--- /dev/null
+++ b/src/com/android/calendar/AgendaWindowAdapter.java
@@ -0,0 +1,833 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.calendar;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Calendar.Attendees;
+import android.provider.Calendar.Calendars;
+import android.provider.Calendar.Instances;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import java.util.Formatter;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+/*
+Bugs Bugs Bugs:
+- At rotation and launch time, the initial position is not set properly. This code is calling
+ listview.setSelection() in 2 rapid secessions but it dropped or didn't process the first one.
+- Query of 2009 11 09 to 2010 01 15 didnt't return anything. In fact, Query of 2010 is showing nothing
+- Scroll using trackball isn't repositioning properly after a new adapter is added.
+- Potential ping pong effect if the prefetch window is big and data is limited
+- Add index in calendar provider
+
+ToDo ToDo ToDo:
+Remove scrollbars
+Remove debug P:XXX from event text
+Get design of header and footer from designer
+
+Make scrolling smoother.
+Test for correctness
+Loading speed
+Check for leaks and excessive allocations
+ */
+
+public class AgendaWindowAdapter extends BaseAdapter {
+
+    private static final boolean BASICLOG = false;
+    private static final boolean DEBUGLOG = false;
+    private static String TAG = "AgendaWindowAdapter";
+
+    private static final String AGENDA_SORT_ORDER = "startDay ASC, begin ASC, title ASC";
+    public static final int INDEX_TITLE = 1;
+    public static final int INDEX_EVENT_LOCATION = 2;
+    public static final int INDEX_ALL_DAY = 3;
+    public static final int INDEX_HAS_ALARM = 4;
+    public static final int INDEX_COLOR = 5;
+    public static final int INDEX_RRULE = 6;
+    public static final int INDEX_BEGIN = 7;
+    public static final int INDEX_END = 8;
+    public static final int INDEX_EVENT_ID = 9;
+    public static final int INDEX_START_DAY = 10;
+    public static final int INDEX_END_DAY = 11;
+    public static final int INDEX_SELF_ATTENDEE_STATUS = 12;
+
+    private static final String[] PROJECTION = new String[] {
+            Instances._ID, // 0
+            Instances.TITLE, // 1
+            Instances.EVENT_LOCATION, // 2
+            Instances.ALL_DAY, // 3
+            Instances.HAS_ALARM, // 4
+            Instances.COLOR, // 5
+            Instances.RRULE, // 6
+            Instances.BEGIN, // 7
+            Instances.END, // 8
+            Instances.EVENT_ID, // 9
+            Instances.START_DAY, // 10 Julian start day
+            Instances.END_DAY, // 11 Julian end day
+            Instances.SELF_ATTENDEE_STATUS, // 12
+    };
+
+    // Listview may have a bug where the index/position is not consistent when there's a header.
+    // TODO Need to look into this.
+    private static final int OFF_BY_ONE_BUG = 1;
+
+    private static final int MAX_NUM_OF_ADAPTERS = 5;
+
+    private static final int IDEAL_NUM_OF_EVENTS = 50;
+
+    private static final int MIN_QUERY_DURATION = 7; // days
+
+    private static final int MAX_QUERY_DURATION = 60; // days
+
+    private static final int PREFETCH_BOUNDARY = 1;
+
+    // Times to auto-expand/retry query after getting no data
+    private static final int RETRIES_ON_NO_DATA = 0;
+
+    private Context mContext;
+
+    private QueryHandler mQueryHandler;
+
+    private AgendaListView mAgendaListView;
+
+    private int mRowCount; // The sum of the rows in all the adapters
+
+    private int mEmptyCursorCount;
+
+    private DayAdapterInfo mLastUsedInfo; // Cached value of the last used adapter.
+
+    private LinkedList<DayAdapterInfo> mAdapterInfos = new LinkedList<DayAdapterInfo>();
+
+    private ConcurrentLinkedQueue<QuerySpec> mQueryQueue = new ConcurrentLinkedQueue<QuerySpec>();
+
+    private TextView mHeaderView;
+
+    private TextView mFooterView;
+
+    private boolean mDoneSettingUpHeaderFooter = false;
+
+    /*
+     * When the user scrolled to the top, a query will be made for older events
+     * and this will be incremented. Don't make more requests if
+     * mOlderRequests > mOlderRequestsProcessed.
+     */
+    private int mOlderRequests;
+
+    // Number of "older" query that has been processed.
+    private int mOlderRequestsProcessed;
+
+    /*
+     * When the user scrolled to the bottom, a query will be made for newer
+     * events and this will be incremented. Don't make more requests if
+     * mNewerRequests > mNewerRequestsProcessed.
+     */
+    private int mNewerRequests;
+
+    // Number of "newer" query that has been processed.
+    private int mNewerRequestsProcessed;
+
+    private Formatter mFormatter; // TODO fix. not thread safe
+
+    private StringBuilder mStringBuilder;
+
+    private boolean mShuttingDown;
+    private boolean mHideDeclined;
+
+    // Types of Query
+    private static final int QUERY_TYPE_OLDER = 0; // Query for older events
+    private static final int QUERY_TYPE_NEWER = 1; // Query for newer events
+    private static final int QUERY_TYPE_CLEAN = 2; // Delete everything and query around a date
+
+    private static class QuerySpec {
+        long queryStartMillis;
+
+        Time goToTime;
+
+        int start;
+
+        int end;
+
+        int queryType;
+
+        public QuerySpec(int queryType) {
+            this.queryType = queryType;
+        }
+    }
+
+    static class EventInfo {
+        long begin;
+
+        long end;
+
+        long id;
+    }
+
+    private static class DayAdapterInfo {
+        Cursor cursor;
+
+        AgendaByDayAdapter dayAdapter;
+
+        int start; // start day of the cursor's coverage
+
+        int end; // end day of the cursor's coverage
+
+        int offset; // offset in position in the list view
+
+        int size; // dayAdapter.getCount()
+
+        public DayAdapterInfo(Context context) {
+            dayAdapter = new AgendaByDayAdapter(context);
+        }
+
+        @Override
+        public String toString() {
+            Time time = new Time();
+            StringBuilder sb = new StringBuilder();
+            time.setJulianDay(start);
+            time.normalize(false);
+            sb.append("Start:").append(time.toString());
+            time.setJulianDay(end);
+            time.normalize(false);
+            sb.append(" End:").append(time.toString());
+            sb.append(" Offset:").append(offset);
+            sb.append(" Size:").append(size);
+            return sb.toString();
+        }
+    }
+
+    public AgendaWindowAdapter(AgendaActivity agendaActivity,
+            AgendaListView agendaListView) {
+        mContext = agendaActivity;
+        mAgendaListView = agendaListView;
+        mQueryHandler = new QueryHandler(agendaActivity.getContentResolver());
+
+        mStringBuilder = new StringBuilder(50);
+        mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
+
+        LayoutInflater inflater = (LayoutInflater) agendaActivity
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mHeaderView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
+        mFooterView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
+        mHeaderView.setText(R.string.loading);
+        mAgendaListView.addHeaderView(mHeaderView);
+    }
+
+    // Method in Adapter
+    @Override
+    public int getViewTypeCount() {
+        return AgendaByDayAdapter.TYPE_LAST;
+    }
+
+    // Method in BaseAdapter
+    @Override
+    public boolean areAllItemsEnabled() {
+        return false;
+    }
+
+    // Method in Adapter
+    @Override
+    public int getItemViewType(int position) {
+        DayAdapterInfo info = getAdapterInfoByPosition(position);
+        if (info != null) {
+            return info.dayAdapter.getItemViewType(position - info.offset);
+        } else {
+            return -1;
+        }
+    }
+
+    // Method in BaseAdapter
+    @Override
+    public boolean isEnabled(int position) {
+        DayAdapterInfo info = getAdapterInfoByPosition(position);
+        if (info != null) {
+            return info.dayAdapter.isEnabled(position - info.offset);
+        } else {
+            return false;
+        }
+    }
+
+    // Abstract Method in BaseAdapter
+    public int getCount() {
+        return mRowCount;
+    }
+
+    // Abstract Method in BaseAdapter
+    public Object getItem(int position) {
+        DayAdapterInfo info = getAdapterInfoByPosition(position);
+        if (info != null) {
+            return info.dayAdapter.getItem(position - info.offset);
+        } else {
+            return null;
+        }
+    }
+
+    // Abstract Method in BaseAdapter
+    public long getItemId(int position) {
+        DayAdapterInfo info = getAdapterInfoByPosition(position);
+        if (info != null) {
+            return info.dayAdapter.getItemId(position - info.offset);
+        } else {
+            return -1;
+        }
+    }
+
+    // Abstract Method in BaseAdapter
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (position >= (mRowCount - PREFETCH_BOUNDARY)
+                && mNewerRequests <= mNewerRequestsProcessed) {
+            if (DEBUGLOG) Log.e(TAG, "queryForNewerEvents: ");
+            mNewerRequests++;
+            queueQuery(new QuerySpec(QUERY_TYPE_NEWER));
+        }
+
+        if (position < PREFETCH_BOUNDARY
+                && mOlderRequests <= mOlderRequestsProcessed) {
+            if (DEBUGLOG) Log.e(TAG, "queryForOlderEvents: ");
+            mOlderRequests++;
+            queueQuery(new QuerySpec(QUERY_TYPE_OLDER));
+        }
+
+        View v;
+        DayAdapterInfo info = getAdapterInfoByPosition(position);
+        if (info != null) {
+            v = info.dayAdapter.getView(position - info.offset, convertView,
+                    parent);
+        } else {
+            //TODO
+            Log.e(TAG, "BUG: getAdapterInfoByPosition returned null!!! " + position);
+            TextView tv = new TextView(mContext);
+            tv.setText("Bug! " + position);
+            v = tv;
+        }
+
+        if (DEBUGLOG) {
+            Log.e(TAG, "getView " + position + " = " + getViewTitle(v));
+        }
+        return v;
+    }
+
+    private int findDayPositionNearestTime(Time time) {
+        if (DEBUGLOG) Log.e(TAG, "findDayPositionNearestTime " + time);
+
+        DayAdapterInfo info = getAdapterInfoByTime(time);
+        if (info != null) {
+            return info.offset + info.dayAdapter.findDayPositionNearestTime(time);
+        } else {
+            return -1;
+        }
+    }
+
+    private DayAdapterInfo getAdapterInfoByPosition(int position) {
+        synchronized (mAdapterInfos) {
+            if (mLastUsedInfo != null && mLastUsedInfo.offset <= position
+                    && position < (mLastUsedInfo.offset + mLastUsedInfo.size)) {
+                return mLastUsedInfo;
+            }
+            for (DayAdapterInfo info : mAdapterInfos) {
+                if (info.offset <= position
+                        && position < (info.offset + info.size)) {
+                    mLastUsedInfo = info;
+                    return info;
+                }
+            }
+        }
+        return null;
+    }
+
+    private DayAdapterInfo getAdapterInfoByTime(Time time) {
+        if (DEBUGLOG) Log.e(TAG, "getAdapterInfoByTime " + time.toString());
+
+        Time tmpTime = new Time(time);
+        long timeInMillis = tmpTime.normalize(true);
+        int day = Time.getJulianDay(timeInMillis, tmpTime.gmtoff);
+        synchronized (mAdapterInfos) {
+            for (DayAdapterInfo info : mAdapterInfos) {
+                if (info.start <= day && day < info.end) {
+                    return info;
+                }
+            }
+        }
+        return null;
+    }
+
+    public EventInfo getEventByPosition(int position) {
+        if (DEBUGLOG) Log.e(TAG, "getEventByPosition " + position);
+
+        EventInfo event = new EventInfo();
+        position -= OFF_BY_ONE_BUG;
+        DayAdapterInfo info = getAdapterInfoByPosition(position);
+        if (info == null) {
+            return null;
+        }
+
+        position = info.dayAdapter.getCursorPosition(position - info.offset);
+        if (position == Integer.MIN_VALUE) {
+            return null;
+        }
+
+        boolean isDayHeader = false;
+        if (position < 0) {
+            position = -position;
+            isDayHeader = true;
+        }
+
+        if (position < info.cursor.getCount()) {
+            info.cursor.moveToPosition(position);
+            event.begin = info.cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN);
+            if (isDayHeader) {
+                Time time = new Time();
+                time.set(event.begin);
+                time.hour = 0;
+                time.minute = 0;
+                time.second = 0;
+                event.begin = time.toMillis(false /* use isDst */);
+            } else {
+                event.end = info.cursor.getLong(AgendaWindowAdapter.INDEX_END);
+                event.id = info.cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID);
+            }
+            return event;
+        }
+        return null;
+    }
+
+    public void refresh(Time goToTime, boolean forced) {
+        Time tmpTime = new Time(goToTime);
+        long goToTimeInMillis = tmpTime.normalize(true); //TODO check on ignoreDst
+        int startDay = Time.getJulianDay(goToTimeInMillis, tmpTime.gmtoff);
+
+        if (!forced && isInRange(startDay, startDay)) {
+            // No need to requery
+            mAgendaListView.setSelection(findDayPositionNearestTime(goToTime) + OFF_BY_ONE_BUG);
+            return;
+        }
+
+        // Query for 2 days before the start day for a total of MIN_QUERY_DURATION days
+        startDay -= 2;
+        int endDay = startDay + MIN_QUERY_DURATION;
+
+        queueQuery(startDay, endDay, goToTime, QUERY_TYPE_CLEAN);
+    }
+
+    public void close() {
+        mShuttingDown = true;
+        pruneAdapterInfo(QUERY_TYPE_CLEAN);
+        if (mQueryHandler != null) {
+            mQueryHandler.cancelOperation(0);
+        }
+    }
+
+    private DayAdapterInfo pruneAdapterInfo(int queryType) {
+        synchronized (mAdapterInfos) {
+            DayAdapterInfo recycleMe = null;
+            if (!mAdapterInfos.isEmpty()) {
+                if (mAdapterInfos.size() >= MAX_NUM_OF_ADAPTERS) {
+                    if (queryType == QUERY_TYPE_NEWER) {
+                        recycleMe = mAdapterInfos.removeFirst();
+                    } else if (queryType == QUERY_TYPE_OLDER) {
+                        recycleMe = mAdapterInfos.removeLast();
+                        // Keep the size only if the oldest items are removed.
+                        recycleMe.size = 0;
+                    }
+                    if (recycleMe != null) {
+                        if (recycleMe.cursor != null) {
+                            recycleMe.cursor.close();
+                        }
+                        return recycleMe;
+                    }
+                }
+
+                if (mRowCount == 0 || queryType == QUERY_TYPE_CLEAN) {
+                    mRowCount = 0;
+                    int deletedRows = 0;
+                    DayAdapterInfo info;
+                    do {
+                        info = mAdapterInfos.poll();
+                        if (info != null) {
+                            info.cursor.close();
+                            deletedRows += info.size;
+                            recycleMe = info;
+                        }
+                    } while (info != null);
+
+                    if (recycleMe != null) {
+                        recycleMe.cursor = null;
+                        recycleMe.size = deletedRows;
+                    }
+                }
+            }
+            return recycleMe;
+        }
+    }
+
+    private String buildQuerySelection() {
+        // Respect the preference to show/hide declined events
+
+        if (mHideDeclined) {
+            return Calendars.SELECTED + "=1 AND "
+                    + Instances.SELF_ATTENDEE_STATUS + "!="
+                    + Attendees.ATTENDEE_STATUS_DECLINED;
+        } else {
+            return Calendars.SELECTED + "=1";
+        }
+    }
+
+    private Uri buildQueryUri(int start, int end) {
+        StringBuilder path = new StringBuilder();
+        path.append(start);
+        path.append('/');
+        path.append(end);
+        Uri uri = Uri.withAppendedPath(Instances.CONTENT_BY_DAY_URI, path.toString());
+        return uri;
+    }
+
+    private boolean isInRange(int start, int end) {
+        synchronized (mAdapterInfos) {
+            if (mAdapterInfos.isEmpty()) {
+                return false;
+            }
+            return mAdapterInfos.getFirst().start <= start && end <= mAdapterInfos.getLast().end;
+        }
+    }
+
+    private int calculateQueryDuration(int start, int end) {
+        int queryDuration = MAX_QUERY_DURATION;
+        if (mRowCount != 0) {
+            queryDuration = IDEAL_NUM_OF_EVENTS * (end - start + 1) / mRowCount;
+        }
+
+        if (queryDuration > MAX_QUERY_DURATION) {
+            queryDuration = MAX_QUERY_DURATION;
+        } else if (queryDuration < MIN_QUERY_DURATION) {
+            queryDuration = MIN_QUERY_DURATION;
+        }
+
+        return queryDuration;
+    }
+
+    private boolean queueQuery(int start, int end, Time goToTime, int queryType) {
+        QuerySpec queryData = new QuerySpec(queryType);
+        queryData.goToTime = goToTime;
+        queryData.start = start;
+        queryData.end = end;
+        return queueQuery(queryData);
+    }
+
+    private boolean queueQuery(QuerySpec queryData) {
+        Boolean queuedQuery;
+        synchronized (mQueryQueue) {
+            queuedQuery = false;
+            Boolean doQueryNow = mQueryQueue.isEmpty();
+            if (!isInRange(queryData.start, queryData.end)) {
+                mQueryQueue.add(queryData);
+                queuedQuery = true;
+            }
+            if (doQueryNow) {
+                doQuery(queryData);
+            }
+        }
+        return queuedQuery;
+    }
+
+    private void doQuery(QuerySpec queryData) {
+        if (!mAdapterInfos.isEmpty()) {
+            int start = mAdapterInfos.getFirst().start;
+            int end = mAdapterInfos.getLast().end;
+            int queryDuration = calculateQueryDuration(start, end);
+            switch(queryData.queryType) {
+                case QUERY_TYPE_OLDER:
+                    queryData.end = start - 1;
+                    queryData.start = queryData.end - queryDuration;
+                    break;
+                case QUERY_TYPE_NEWER:
+                    queryData.start = end + 1;
+                    queryData.end = queryData.start + queryDuration;
+                    break;
+            }
+        }
+
+        if (BASICLOG) {
+            Time time = new Time();
+            time.setJulianDay(queryData.start);
+            Time time2 = new Time();
+            time2.setJulianDay(queryData.end);
+            Log.v(TAG, "startQuery: " + time.toString() + " to "
+                    + time2.toString() + " then go to " + queryData.goToTime);
+        }
+
+        mQueryHandler.cancelOperation(0);
+        if (BASICLOG) queryData.queryStartMillis = System.nanoTime();
+        mQueryHandler.startQuery(0, queryData, buildQueryUri(
+                queryData.start, queryData.end), PROJECTION,
+                buildQuerySelection(), null, AGENDA_SORT_ORDER);
+    }
+
+    private String formatDateString(int julianDay) {
+        Time time = new Time();
+        time.setJulianDay(julianDay);
+        long millis = time.toMillis(false);
+        mStringBuilder.setLength(0);
+        return DateUtils.formatDateRange(mContext, mFormatter, millis, millis,
+                DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE
+                        | DateUtils.FORMAT_ABBREV_MONTH).toString();
+    }
+
+    private void updateHeaderFooter(final int start, final int end) {
+        mHeaderView.setText(mContext.getString(R.string.show_older_events,
+                formatDateString(start)));
+        mFooterView.setText(mContext.getString(R.string.show_newer_events,
+                formatDateString(end)));
+    }
+
+    private class QueryHandler extends AsyncQueryHandler {
+
+        public QueryHandler(ContentResolver cr) {
+            super(cr);
+        }
+
+        @Override
+        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+            QuerySpec data = (QuerySpec)cookie;
+            if (BASICLOG) {
+                long queryEndMillis = System.nanoTime();
+                Log.e(TAG, "Query time(ms): "
+                        + (queryEndMillis - data.queryStartMillis) / 1000000
+                        + " Count: " + cursor.getCount());
+            }
+
+            if (mShuttingDown) {
+                cursor.close();
+                return;
+            }
+
+            // Notify Listview of changes and update position
+            int cursorSize = cursor.getCount();
+            if (cursorSize > 0 || mAdapterInfos.isEmpty()) {
+                final int listPositionOffset = processNewCursor(data, cursor);
+                if (data.goToTime == null) { // Typical Scrolling type query
+                    notifyDataSetChanged();
+                    if (listPositionOffset != 0) {
+                        mAgendaListView.shiftSelection(listPositionOffset);
+                    }
+                } else { // refresh() called. Go to the designated position
+                    final Time goToTime = data.goToTime;
+                    notifyDataSetChanged();
+                    int newPosition = findDayPositionNearestTime(goToTime);
+                    if (newPosition >= 0) {
+                        mAgendaListView.setSelection(newPosition + OFF_BY_ONE_BUG);
+                    }
+                    if (DEBUGLOG)
+                        Log.e(TAG, "Setting listview to " +
+                        		"findDayPositionNearestTime: " + (newPosition + OFF_BY_ONE_BUG));
+                }
+            } else {
+                cursor.close();
+            }
+
+            // Update header and footer
+            if (!mDoneSettingUpHeaderFooter) {
+                OnClickListener headerFooterOnClickListener = new OnClickListener() {
+                    public void onClick(View v) {
+                        if (v == mHeaderView) {
+                            queueQuery(new QuerySpec(QUERY_TYPE_OLDER));
+                        } else {
+                            queueQuery(new QuerySpec(QUERY_TYPE_NEWER));
+                        }
+                    }};
+                mHeaderView.setOnClickListener(headerFooterOnClickListener);
+                mFooterView.setOnClickListener(headerFooterOnClickListener);
+                mAgendaListView.addFooterView(mFooterView);
+                mDoneSettingUpHeaderFooter = true;
+            }
+            synchronized (mQueryQueue) {
+                int totalAgendaRangeStart = -1;
+                int totalAgendaRangeEnd = -1;
+
+                if (cursorSize != 0) {
+                    // TODO check if it's same as "cookie"
+                    // Remove the query that just completed
+                    QuerySpec x = mQueryQueue.poll();
+                    mEmptyCursorCount = 0;
+                    mOlderRequests = mOlderRequestsProcessed;
+                    mNewerRequests = mNewerRequestsProcessed;
+
+                    totalAgendaRangeStart = mAdapterInfos.getFirst().start;
+                    totalAgendaRangeEnd = mAdapterInfos.getLast().end;
+                } else { // CursorSize == 0
+                    QuerySpec querySpec = mQueryQueue.peek();
+
+                    // Update Adapter Info with new start and end date range
+                    if (!mAdapterInfos.isEmpty()) {
+                        DayAdapterInfo first = mAdapterInfos.getFirst();
+                        DayAdapterInfo last = mAdapterInfos.getLast();
+
+                        if (first.start - 1 <= querySpec.end && querySpec.start < first.start) {
+                            first.start = querySpec.start;
+                        }
+
+                        if (querySpec.start <= last.end + 1 && last.end < querySpec.end) {
+                            last.end = querySpec.end;
+                        }
+
+                        totalAgendaRangeStart = first.start;
+                        totalAgendaRangeEnd = last.end;
+                    } else {
+                        totalAgendaRangeStart = querySpec.start;
+                        totalAgendaRangeEnd = querySpec.end;
+                    }
+
+                    // Update query specification with expanded search range
+                    // and maybe rerun query
+                    switch (querySpec.queryType) {
+                        case QUERY_TYPE_OLDER:
+                            totalAgendaRangeStart = querySpec.start;
+                            querySpec.start -= MAX_QUERY_DURATION;
+                            break;
+                        case QUERY_TYPE_NEWER:
+                            totalAgendaRangeEnd = querySpec.end;
+                            querySpec.end += MAX_QUERY_DURATION;
+                            break;
+                        case QUERY_TYPE_CLEAN:
+                            totalAgendaRangeStart = querySpec.start;
+                            totalAgendaRangeEnd = querySpec.end;
+                            querySpec.start -= MAX_QUERY_DURATION / 2;
+                            querySpec.end += MAX_QUERY_DURATION / 2;
+                            break;
+                    }
+
+                    if (++mEmptyCursorCount > RETRIES_ON_NO_DATA) {
+                        // Nothing in the cursor again. Dropping query
+                        mQueryQueue.poll();
+                    }
+                }
+
+                updateHeaderFooter(totalAgendaRangeStart, totalAgendaRangeEnd);
+
+                // Fire off the next query if any
+                Iterator<QuerySpec> it = mQueryQueue.iterator();
+                while (it.hasNext()) {
+                    QuerySpec queryData = it.next();
+                    if (!isInRange(queryData.start, queryData.end)) {
+                        // Query accepted
+                        if (DEBUGLOG) Log.e(TAG, "Query accepted. QueueSize:" + mQueryQueue.size());
+                        doQuery(queryData);
+                        break;
+                    } else {
+                        // Query rejected
+                        it.remove();
+                        if (DEBUGLOG) Log.e(TAG, "Query rejected. QueueSize:" + mQueryQueue.size());
+                    }
+                }
+            }
+            if (DEBUGLOG) {
+                for (DayAdapterInfo info3 : mAdapterInfos) {
+                    Log.e(TAG, "> " + info3.toString());
+                }
+            }
+        }
+
+        /*
+         * Update the adapter info array with a the new cursor. Close out old
+         * cursors as needed.
+         *
+         * @return number of rows removed from the beginning
+         */
+        private int processNewCursor(QuerySpec data, Cursor cursor) {
+            synchronized (mAdapterInfos) {
+                // Remove adapter info's from adapterInfos as needed
+                DayAdapterInfo info = pruneAdapterInfo(data.queryType);
+                int listPositionOffset = 0;
+                if (info == null) {
+                    info = new DayAdapterInfo(mContext);
+                } else {
+                    if (DEBUGLOG)
+                        Log.e(TAG, "processNewCursor listPositionOffsetA="
+                                + -info.size);
+                    listPositionOffset = -info.size;
+                }
+
+                // Setup adapter info
+                info.start = data.start;
+                info.end = data.end;
+                info.cursor = cursor;
+                info.dayAdapter.changeCursor(cursor);
+                info.size = info.dayAdapter.getCount();
+
+                // Insert into adapterInfos
+                if (mAdapterInfos.isEmpty()
+                        || data.end <= mAdapterInfos.getFirst().start) {
+                    mAdapterInfos.addFirst(info);
+                    listPositionOffset += info.size;
+                } else if (BASICLOG && data.start < mAdapterInfos.getLast().end) {
+                    mAdapterInfos.addLast(info);
+                    for (DayAdapterInfo info2 : mAdapterInfos) {
+                        Log.e("========== BUG ==", info2.toString());
+                    }
+                } else {
+                    mAdapterInfos.addLast(info);
+                }
+
+                // Update offsets in adapterInfos
+                mRowCount = 0;
+                for (DayAdapterInfo info3 : mAdapterInfos) {
+                    info3.offset = mRowCount;
+                    mRowCount += info3.size;
+                }
+                mLastUsedInfo = null;
+
+                return listPositionOffset;
+            }
+        }
+    }
+
+    static String getViewTitle(View x) {
+        String title = "";
+        if (x != null) {
+            Object yy = x.getTag();
+            if (yy instanceof AgendaAdapter.ViewHolder) {
+                TextView tv = ((AgendaAdapter.ViewHolder) yy).title;
+                if (tv != null) {
+                    title = (String) tv.getText();
+                }
+            } else if (yy != null) {
+                TextView dateView = ((AgendaByDayAdapter.ViewHolder) yy).dateView;
+                if (dateView != null) {
+                    title = (String) dateView.getText();
+                }
+            }
+        }
+        return title;
+    }
+
+    public void setHideDeclinedEvents(boolean hideDeclined) {
+        mHideDeclined = hideDeclined;
+    }
+}