From 13850936e579386a0f2ee589607bbf8b7cf1a7d4 Mon Sep 17 00:00:00 2001 From: Michael Chan Date: Tue, 30 Jun 2009 15:35:34 -0700 Subject: 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 --- res/layout/agenda_header_footer.xml | 27 + res/values/strings.xml | 6 + src/com/android/calendar/AgendaActivity.java | 316 ++------ src/com/android/calendar/AgendaAdapter.java | 35 +- src/com/android/calendar/AgendaByDayAdapter.java | 84 ++- src/com/android/calendar/AgendaListView.java | 200 ++++++ src/com/android/calendar/AgendaWindowAdapter.java | 833 ++++++++++++++++++++++ 7 files changed, 1208 insertions(+), 293 deletions(-) create mode 100644 res/layout/agenda_header_footer.xml create mode 100644 src/com/android/calendar/AgendaListView.java create mode 100644 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 00000000..96824f50 --- /dev/null +++ b/res/layout/agenda_header_footer.xml @@ -0,0 +1,27 @@ + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index e49015e9..c463ef6d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -230,6 +230,12 @@ Today + + Loading\u2026 + + Showing events since %1$s. Tap to look for more. + + Showing events until %1$s. Tap to look for more. diff --git a/src/com/android/calendar/AgendaActivity.java b/src/com/android/calendar/AgendaActivity.java index 9f5a53ed..15c9387f 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 { - protected static final String BUNDLE_KEY_RESTORE_TIME = "key_restore_time"; + private static final String TAG = "AgendaActivity"; - 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 - }; + private static boolean DEBUG = false; - 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"; + protected static final String BUNDLE_KEY_RESTORE_TIME = "key_restore_time"; 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 @@ public class AgendaActivity extends Activity implements ViewSwitcher.ViewFactory 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 @@ public class AgendaActivity extends Activity implements ViewSwitcher.ViewFactory @Override public void onChange(boolean selfChange) { - clearLastQueryTime(); - renewCursor(); + mAgendaListView.refresh(true); } }; @@ -206,26 +85,42 @@ public class AgendaActivity extends Activity implements ViewSwitcher.ViewFactory // 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 + setTitle(R.string.agenda_view); + + long millis = 0; mTime = new Time(); if (icicle != null) { - mTime.set(icicle.getLong(BUNDLE_KEY_RESTORE_TIME)); - } else { - mTime.set(Utils.timeFromIntent(getIntent())); + // Returns 0 if key not found + millis = icicle.getLong(BUNDLE_KEY_RESTORE_TIME); + if (DEBUG) { + Log.v(TAG, "Restore value from icicle: " + millis); + } } - setTitle(R.string.agenda_view); - mViewSwitcher = (ViewSwitcher) findViewById(R.id.switcher); - mViewSwitcher.setFactory(this); + 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 @@ public class AgendaActivity extends Activity implements ViewSwitcher.ViewFactory // 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()); + } + + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + boolean hideDeclined = prefs + .getBoolean(CalendarPreferenceActivity.KEY_HIDE_DECLINED, false); - clearLastQueryTime(); - renewCursor(); + mAgendaListView.setHideDeclinedEvents(hideDeclined); + mAgendaListView.goTo(mTime, true); // Register for Intent broadcasts IntentFilter filter = new IntentFilter(); @@ -258,7 +159,14 @@ public class AgendaActivity extends Activity implements ViewSwitcher.ViewFactory 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 @@ public class AgendaActivity extends Activity implements ViewSwitcher.ViewFactory @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 @@ public class AgendaActivity extends Activity implements ViewSwitcher.ViewFactory 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 class AgendaActivity extends Activity implements ViewSwitcher.ViewFactory } 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 69649eaf..8603fc70 100644 --- a/src/com/android/calendar/AgendaAdapter.java +++ b/src/com/android/calendar/AgendaAdapter.java @@ -26,10 +26,15 @@ import android.view.View; 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 @@ public class AgendaAdapter extends ResourceCursorAdapter { 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 @@ public class AgendaAdapter extends ResourceCursorAdapter { } // 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 @@ public class AgendaAdapter extends ResourceCursorAdapter { 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 @@ public class AgendaAdapter extends ResourceCursorAdapter { 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 @@ public class AgendaAdapter extends ResourceCursorAdapter { 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 @@ public class AgendaAdapter extends ResourceCursorAdapter { */ // 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 140eb727..5d26e3df 100644 --- a/src/com/android/calendar/AgendaByDayAdapter.java +++ b/src/com/android/calendar/AgendaByDayAdapter.java @@ -27,26 +27,35 @@ import android.widget.BaseAdapter; 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 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 @@ public class AgendaByDayAdapter extends BaseAdapter { 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 @@ public class AgendaByDayAdapter extends BaseAdapter { 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 @@ public class AgendaByDayAdapter extends BaseAdapter { mRowInfo = null; } + public void changeCursor(Cursor cursor) { + calculateDays(cursor); + mAgendaAdapter.changeCursor(cursor); + } + public void calculateDays(Cursor cursor) { ArrayList rowInfo = new ArrayList(); int prevStartDay = -1; @@ -154,8 +177,8 @@ public class AgendaByDayAdapter extends BaseAdapter { mTodayJulianDay = Time.getJulianDay(now, time.gmtoff); LinkedList multipleDayList = new LinkedList(); 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 @@ public class AgendaByDayAdapter extends BaseAdapter { // 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 @@ public class AgendaByDayAdapter extends BaseAdapter { // 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 @@ public class AgendaByDayAdapter extends BaseAdapter { 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 @@ public class AgendaByDayAdapter extends BaseAdapter { return true; } } - diff --git a/src/com/android/calendar/AgendaListView.java b/src/com/android/calendar/AgendaListView.java new file mode 100644 index 00000000..09bd7ede --- /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 00000000..cede1900 --- /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 mAdapterInfos = new LinkedList(); + + private ConcurrentLinkedQueue mQueryQueue = new ConcurrentLinkedQueue(); + + 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 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; + } +} -- cgit v1.2.3