summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Chan <mchan@android.com>2009-06-30 15:35:34 -0700
committerMichael Chan <mchan@android.com>2009-07-10 17:58:23 -0700
commit13850936e579386a0f2ee589607bbf8b7cf1a7d4 (patch)
treea7c105b0a7ed15308e84337f6dc6d81bda8d7386
parentb12ecbb90d06018fa092a2ac8253767fb62ed092 (diff)
downloadandroid_packages_apps_Calendar-13850936e579386a0f2ee589607bbf8b7cf1a7d4.tar.gz
android_packages_apps_Calendar-13850936e579386a0f2ee589607bbf8b7cf1a7d4.tar.bz2
android_packages_apps_Calendar-13850936e579386a0f2ee589607bbf8b7cf1a7d4.zip
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
-rw-r--r--res/layout/agenda_header_footer.xml27
-rw-r--r--res/values/strings.xml6
-rw-r--r--src/com/android/calendar/AgendaActivity.java316
-rw-r--r--src/com/android/calendar/AgendaAdapter.java35
-rw-r--r--src/com/android/calendar/AgendaByDayAdapter.java84
-rw-r--r--src/com/android/calendar/AgendaListView.java200
-rw-r--r--src/com/android/calendar/AgendaWindowAdapter.java833
7 files changed, 1208 insertions, 293 deletions
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 @@
+<?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 e49015e9..c463ef6d 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 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<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 @@ 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> rowInfo = new ArrayList<RowInfo>();
int prevStartDay = -1;
@@ -154,8 +177,8 @@ public class AgendaByDayAdapter extends BaseAdapter {
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 @@ 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<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;
+ }
+}