summaryrefslogtreecommitdiffstats
path: root/src/com
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:32:18 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:32:18 -0800
commit146de36083f6ce8b7e8a1f974d3990594a36bfec (patch)
tree26291db8f35326f89276b7f51dda5b5b4e78f070 /src/com
parent2cb8df4a54d65554c34faa79d8b2a46a86ff7b52 (diff)
downloadandroid_packages_apps_Calendar-146de36083f6ce8b7e8a1f974d3990594a36bfec.tar.gz
android_packages_apps_Calendar-146de36083f6ce8b7e8a1f974d3990594a36bfec.tar.bz2
android_packages_apps_Calendar-146de36083f6ce8b7e8a1f974d3990594a36bfec.zip
auto import from //depot/cupcake/@135843
Diffstat (limited to 'src/com')
-rw-r--r--src/com/android/calendar/AgendaActivity.java428
-rw-r--r--src/com/android/calendar/AgendaAdapter.java158
-rw-r--r--src/com/android/calendar/AgendaByDayAdapter.java364
-rw-r--r--src/com/android/calendar/AlertActivity.java284
-rw-r--r--src/com/android/calendar/AlertAdapter.java108
-rw-r--r--src/com/android/calendar/AlertReceiver.java234
-rw-r--r--src/com/android/calendar/AlertService.java437
-rw-r--r--src/com/android/calendar/CalendarActivity.java360
-rw-r--r--src/com/android/calendar/CalendarApplication.java84
-rw-r--r--src/com/android/calendar/CalendarData.java55
-rw-r--r--src/com/android/calendar/CalendarPreferenceActivity.java85
-rw-r--r--src/com/android/calendar/CalendarView.java2970
-rw-r--r--src/com/android/calendar/DayActivity.java72
-rw-r--r--src/com/android/calendar/DayView.java33
-rw-r--r--src/com/android/calendar/DeleteEventHelper.java344
-rw-r--r--src/com/android/calendar/EditEvent.java1690
-rw-r--r--src/com/android/calendar/EditResponseHelper.java92
-rw-r--r--src/com/android/calendar/Event.java625
-rw-r--r--src/com/android/calendar/EventGeometry.java221
-rw-r--r--src/com/android/calendar/EventInfoActivity.java724
-rw-r--r--src/com/android/calendar/EventLoader.java256
-rw-r--r--src/com/android/calendar/IcsImportActivity.java227
-rw-r--r--src/com/android/calendar/LaunchActivity.java98
-rw-r--r--src/com/android/calendar/MenuHelper.java181
-rw-r--r--src/com/android/calendar/MonthActivity.java341
-rw-r--r--src/com/android/calendar/MonthView.java1366
-rw-r--r--src/com/android/calendar/Navigator.java45
-rw-r--r--src/com/android/calendar/SelectCalendarsActivity.java279
-rw-r--r--src/com/android/calendar/SelectCalendarsAdapter.java125
-rw-r--r--src/com/android/calendar/Utils.java104
-rw-r--r--src/com/android/calendar/WeekActivity.java83
-rw-r--r--src/com/android/calendar/WeekView.java33
32 files changed, 12506 insertions, 0 deletions
diff --git a/src/com/android/calendar/AgendaActivity.java b/src/com/android/calendar/AgendaActivity.java
new file mode 100644
index 00000000..fd159e62
--- /dev/null
+++ b/src/com/android/calendar/AgendaActivity.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2007 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.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.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 {
+
+ protected static final String BUNDLE_KEY_RESTORE_TIME = "key_restore_time";
+
+ static final String[] PROJECTION = new String[] {
+ Instances._ID, // 0
+ Instances.TITLE, // 1
+ Instances.EVENT_LOCATION, // 2
+ Instances.ALL_DAY, // 3
+ Instances.HAS_ALARM, // 4
+ Instances.COLOR, // 5
+ Instances.RRULE, // 6
+ Instances.BEGIN, // 7
+ Instances.END, // 8
+ Instances.EVENT_ID, // 9
+ Instances.START_DAY, // 10 Julian start day
+ Instances.END_DAY, // 11 Julian end day
+ Instances.SELF_ATTENDEE_STATUS, // 12
+ };
+
+ public static final int INDEX_TITLE = 1;
+ public static final int INDEX_EVENT_LOCATION = 2;
+ public static final int INDEX_ALL_DAY = 3;
+ public static final int INDEX_HAS_ALARM = 4;
+ public static final int INDEX_COLOR = 5;
+ public static final int INDEX_RRULE = 6;
+ public static final int INDEX_BEGIN = 7;
+ public static final int INDEX_END = 8;
+ public static final int INDEX_EVENT_ID = 9;
+ public static final int INDEX_START_DAY = 10;
+ public static final int INDEX_END_DAY = 11;
+ public static final int INDEX_SELF_ATTENDEE_STATUS = 12;
+
+ public static final String AGENDA_SORT_ORDER = "startDay ASC, begin ASC, title ASC";
+
+ private static final long INITIAL_HEAP_SIZE = 4*1024*1024;
+
+ private ContentResolver mContentResolver;
+
+ private ViewSwitcher mViewSwitcher;
+
+ private 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) {
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_TIME_CHANGED)
+ || action.equals(Intent.ACTION_DATE_CHANGED)
+ || action.equals(Intent.ACTION_TIMEZONE_CHANGED)) {
+ clearLastQueryTime();
+ renewCursor();
+ }
+ }
+ };
+
+ private ContentObserver mObserver = new ContentObserver(new Handler()) {
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ clearLastQueryTime();
+ renewCursor();
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ // Eliminate extra GCs during startup by setting the initial heap size to 4MB.
+ // TODO: We should restore the old heap size once the activity reaches the idle state
+ long oldHeapSize = VMRuntime.getRuntime().setMinimumHeapSize(INITIAL_HEAP_SIZE);
+
+ getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.agenda_activity);
+
+ mContentResolver = getContentResolver();
+ mQueryHandler = new QueryHandler(mContentResolver);
+
+ // Preserve the same month and event selection if this activity is
+ // being restored due to an orientation change
+ mTime = new Time();
+ if (icicle != null) {
+ mTime.set(icicle.getLong(BUNDLE_KEY_RESTORE_TIME));
+ } else {
+ mTime.set(Utils.timeFromIntent(getIntent()));
+ }
+ setTitle(Utils.formatMonthYear(mTime));
+
+ mViewSwitcher = (ViewSwitcher) findViewById(R.id.switcher);
+ mViewSwitcher.setFactory(this);
+
+ // Record Agenda View as the (new) default detailed view.
+ 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);
+
+ // 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();
+
+ clearLastQueryTime();
+ renewCursor();
+
+ // Register for Intent broadcasts
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_TIME_CHANGED);
+ filter.addAction(Intent.ACTION_DATE_CHANGED);
+ filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+ registerReceiver(mIntentReceiver, filter);
+
+ mContentResolver.registerContentObserver(Events.CONTENT_URI, true, mObserver);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putLong(BUNDLE_KEY_RESTORE_TIME, getSelectedTime());
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ mContentResolver.unregisterContentObserver(mObserver);
+ unregisterReceiver(mIntentReceiver);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ MenuHelper.onPrepareOptionsMenu(this, menu);
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuHelper.onCreateOptionsMenu(menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ MenuHelper.onOptionsItemSelected(this, item, this);
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ 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);
+ }
+ }
+ }
+ break;
+
+ case KeyEvent.KEYCODE_BACK:
+ finish();
+ return true;
+ }
+ 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();
+ now.set(System.currentTimeMillis());
+ goTo(now);
+ }
+
+ public void goTo(Time time) {
+ if (mTime.year == time.year && mTime.month == time.month) {
+ mTime = time;
+ selectTime();
+ } else {
+ mTime = time;
+ }
+ }
+
+ 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);
+ }
+
+ public boolean getAllDay() {
+ return false;
+ }
+}
+
diff --git a/src/com/android/calendar/AgendaAdapter.java b/src/com/android/calendar/AgendaAdapter.java
new file mode 100644
index 00000000..6aac218a
--- /dev/null
+++ b/src/com/android/calendar/AgendaAdapter.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2007 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.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.provider.Calendar.Attendees;
+import android.provider.Calendar.Reminders;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ResourceCursorAdapter;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+
+public class AgendaAdapter extends ResourceCursorAdapter {
+
+ static final String[] REMINDERS_PROJECTION = new String[] {
+ Reminders._ID, // 0
+ Reminders.MINUTES, // 1
+ };
+ static final int REMINDERS_INDEX_MINUTES = 1;
+ static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=%d AND (" +
+ Reminders.METHOD + "=" + Reminders.METHOD_ALERT + " OR " + Reminders.METHOD + "=" +
+ Reminders.METHOD_DEFAULT + ")";
+
+ private Resources mResources;
+ private static ArrayList<Integer> sReminderValues;
+ private static String[] sReminderLabels;
+
+ public AgendaAdapter(Context context, int resource) {
+ super(context, resource, null);
+ mResources = context.getResources();
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ // Fade text if event was declined.
+ int selfAttendeeStatus = cursor.getInt(AgendaActivity.INDEX_SELF_ATTENDEE_STATUS);
+ boolean declined = (selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED);
+
+ View stripe = view.findViewById(R.id.vertical_stripe);
+ int color = cursor.getInt(AgendaActivity.INDEX_COLOR);
+ ((FrameLayout) view).setForeground(declined ?
+ mResources.getDrawable(R.drawable.agenda_item_declined) : null);
+
+ stripe.setBackgroundColor(color);
+
+ // What
+ TextView title = (TextView) view.findViewById(R.id.title);
+ String titleString = cursor.getString(AgendaActivity.INDEX_TITLE);
+ if (titleString == null || titleString.length() == 0) {
+ titleString = mResources.getString(R.string.no_title_label);
+ }
+ title.setText(titleString);
+ title.setTextColor(color);
+
+ // When
+ TextView when = (TextView) view.findViewById(R.id.when);
+ long begin = cursor.getLong(AgendaActivity.INDEX_BEGIN);
+ long end = cursor.getLong(AgendaActivity.INDEX_END);
+ boolean allDay = cursor.getInt(AgendaActivity.INDEX_ALL_DAY) != 0;
+ int flags;
+ String whenString;
+ if (allDay) {
+ flags = DateUtils.FORMAT_UTC;
+ } else {
+ flags = DateUtils.FORMAT_SHOW_TIME;
+ }
+ if (DateFormat.is24HourFormat(context)) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ }
+ whenString = DateUtils.formatDateRange(context, begin, end, flags);
+ when.setText(whenString);
+
+ String rrule = cursor.getString(AgendaActivity.INDEX_RRULE);
+ if (rrule != null) {
+ when.setCompoundDrawablesWithIntrinsicBounds(null, null,
+ context.getResources().getDrawable(R.drawable.ic_repeat_dark), null);
+ when.setCompoundDrawablePadding(5);
+ } else {
+ when.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
+ }
+
+ /*
+ // Repeating info
+ View repeatContainer = view.findViewById(R.id.repeat_icon);
+ String rrule = cursor.getString(AgendaActivity.INDEX_RRULE);
+ if (rrule != null) {
+ repeatContainer.setVisibility(View.VISIBLE);
+ } else {
+ repeatContainer.setVisibility(View.GONE);
+ }
+ */
+
+ /*
+ // Reminder
+ boolean hasAlarm = cursor.getInt(AgendaActivity.INDEX_HAS_ALARM) != 0;
+ if (hasAlarm) {
+ updateReminder(view, context, begin, cursor.getLong(AgendaActivity.INDEX_EVENT_ID));
+ }
+ */
+
+ // Where
+ TextView where = (TextView) view.findViewById(R.id.where);
+ String whereString = cursor.getString(AgendaActivity.INDEX_EVENT_LOCATION);
+ if (whereString != null && whereString.length() > 0) {
+ where.setVisibility(View.VISIBLE);
+ where.setText(whereString);
+ } else {
+ where.setVisibility(View.GONE);
+ }
+ }
+
+ /*
+ public static void updateReminder(View view, Context context, long begin, long eventId) {
+ ContentResolver cr = context.getContentResolver();
+ Uri uri = Reminders.CONTENT_URI;
+ String where = String.format(REMINDERS_WHERE, eventId);
+
+ Cursor remindersCursor = cr.query(uri, REMINDERS_PROJECTION, where, null, null);
+ if (remindersCursor != null) {
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ LinearLayout parent = (LinearLayout) view.findViewById(R.id.reminders_container);
+ parent.removeAllViews();
+ while (remindersCursor.moveToNext()) {
+ int alarm = remindersCursor.getInt(REMINDERS_INDEX_MINUTES);
+ String before = EditEvent.constructReminderLabel(context, alarm, true);
+ LinearLayout reminderItem = (LinearLayout)
+ inflater.inflate(R.layout.agenda_reminder_item, null);
+ TextView reminderItemText = (TextView) reminderItem.findViewById(R.id.reminder);
+ reminderItemText.setText(before);
+ parent.addView(reminderItem);
+ }
+ }
+ remindersCursor.close();
+ }
+ */
+}
+
diff --git a/src/com/android/calendar/AgendaByDayAdapter.java b/src/com/android/calendar/AgendaByDayAdapter.java
new file mode 100644
index 00000000..d774efbe
--- /dev/null
+++ b/src/com/android/calendar/AgendaByDayAdapter.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2008 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.Context;
+import android.database.Cursor;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+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;
+
+ private final Context mContext;
+ private final AgendaAdapter mAgendaAdapter;
+ private final LayoutInflater mInflater;
+ private ArrayList<RowInfo> mRowInfo;
+ private int mTodayJulianDay;
+ private Time mTime = new Time();
+
+ public AgendaByDayAdapter(Context context, AgendaAdapter agendaAdapter) {
+ mContext = context;
+ mAgendaAdapter = agendaAdapter;
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ public int getCount() {
+ if (mRowInfo != null) {
+ return mRowInfo.size();
+ }
+ return mAgendaAdapter.getCount();
+ }
+
+ public Object getItem(int position) {
+ if (mRowInfo != null) {
+ RowInfo row = mRowInfo.get(position);
+ if (row.mType == TYPE_DAY) {
+ return row;
+ } else {
+ return mAgendaAdapter.getItem(row.mData);
+ }
+ }
+ return mAgendaAdapter.getItem(position);
+ }
+
+ public long getItemId(int position) {
+ if (mRowInfo != null) {
+ RowInfo row = mRowInfo.get(position);
+ if (row.mType == TYPE_DAY) {
+ return position;
+ } else {
+ return mAgendaAdapter.getItemId(row.mData);
+ }
+ }
+ return mAgendaAdapter.getItemId(position);
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return TYPE_LAST;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return mRowInfo != null && mRowInfo.size() > position ?
+ mRowInfo.get(position).mType : TYPE_DAY;
+ }
+
+ private static class ViewHolder {
+ TextView dateView;
+ TextView dayOfWeekView;
+ }
+
+ 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.
+ return mAgendaAdapter.getView(position, convertView, parent);
+ }
+
+ RowInfo row = mRowInfo.get(position);
+ if (row.mType == TYPE_DAY) {
+ ViewHolder holder;
+ View agendaDayView;
+ if ((convertView == null) || (convertView.getTag() == 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);
+ holder.dayOfWeekView = (TextView) agendaDayView.findViewById(R.id.day_of_week);
+ 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;
+ long millis = date.setJulianDay(row.mData);
+ int flags = DateUtils.FORMAT_NUMERIC_DATE;
+ holder.dateView.setText(DateUtils.formatDateRange(mContext, millis, millis, flags));
+
+ if (row.mData == mTodayJulianDay) {
+ holder.dayOfWeekView.setText(R.string.agenda_today);
+ } else {
+ int weekDay = date.weekDay + Calendar.SUNDAY;
+ holder.dayOfWeekView.setText(DateUtils.getDayOfWeekString(weekDay,
+ DateUtils.LENGTH_LONG));
+ }
+ return agendaDayView;
+ } else if (row.mType == TYPE_MEETING) {
+ return mAgendaAdapter.getView(row.mData, convertView, parent);
+ } else {
+ // Error
+ throw new IllegalStateException("Unknown event type:" + row.mType);
+ }
+ }
+
+ public void clearDayHeaderInfo() {
+ mRowInfo = null;
+ }
+
+ public void calculateDays(Cursor cursor) {
+ ArrayList<RowInfo> rowInfo = new ArrayList<RowInfo>();
+ int prevStartDay = -1;
+ Time time = new Time();
+ long now = System.currentTimeMillis();
+ time.set(now);
+ 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);
+
+ if (startDay != prevStartDay) {
+ // Check if we skipped over any empty days
+ if (prevStartDay == -1) {
+ rowInfo.add(new RowInfo(TYPE_DAY, startDay));
+ } else {
+ // If there are any multiple-day events that span the empty
+ // range of days, then create day headers and events for
+ // those multiple-day events.
+ boolean dayHeaderAdded = false;
+ for (int currentDay = prevStartDay + 1; currentDay <= startDay; currentDay++) {
+ dayHeaderAdded = false;
+ Iterator<MultipleDayInfo> iter = multipleDayList.iterator();
+ while (iter.hasNext()) {
+ MultipleDayInfo info = iter.next();
+ // If this event has ended then remove it from the
+ // list.
+ if (info.mEndDay < currentDay) {
+ iter.remove();
+ continue;
+ }
+
+ // If this is the first event for the day, then
+ // insert a day header.
+ if (!dayHeaderAdded) {
+ rowInfo.add(new RowInfo(TYPE_DAY, currentDay));
+ dayHeaderAdded = true;
+ }
+ rowInfo.add(new RowInfo(TYPE_MEETING, info.mPosition));
+ }
+ }
+
+ // If the day header was not added for the start day, then
+ // add it now.
+ if (!dayHeaderAdded) {
+ rowInfo.add(new RowInfo(TYPE_DAY, startDay));
+ }
+ }
+ prevStartDay = startDay;
+ }
+
+ // Add in the event for this cursor position
+ rowInfo.add(new RowInfo(TYPE_MEETING, position));
+
+ // If this event spans multiple days, then add it to the multipleDay
+ // list.
+ int endDay = cursor.getInt(AgendaActivity.INDEX_END_DAY);
+ if (endDay > startDay) {
+ multipleDayList.add(new MultipleDayInfo(position, endDay));
+ }
+ }
+
+ // There are no more cursor events but we might still have multiple-day
+ // events left. So create day headers and events for those.
+ if (prevStartDay > 0) {
+ // Get the Julian day for the last day of this month. To do that,
+ // 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.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);
+
+ for (int currentDay = prevStartDay + 1; currentDay <= lastDayOfMonth; currentDay++) {
+ boolean dayHeaderAdded = false;
+ Iterator<MultipleDayInfo> iter = multipleDayList.iterator();
+ while (iter.hasNext()) {
+ MultipleDayInfo info = iter.next();
+ // If this event has ended then remove it from the
+ // list.
+ if (info.mEndDay < currentDay) {
+ iter.remove();
+ continue;
+ }
+
+ // If this is the first event for the day, then
+ // insert a day header.
+ if (!dayHeaderAdded) {
+ rowInfo.add(new RowInfo(TYPE_DAY, currentDay));
+ dayHeaderAdded = true;
+ }
+ rowInfo.add(new RowInfo(TYPE_MEETING, info.mPosition));
+ }
+ }
+ }
+ mRowInfo = rowInfo;
+ }
+
+ private static class RowInfo {
+ // mType is either a day header (TYPE_DAY) or an event (TYPE_MEETING)
+ final int mType;
+
+ // If mType is TYPE_DAY, then mData is the Julian day. Otherwise
+ // mType is TYPE_MEETING and mData is the cursor position.
+ final int mData;
+
+ RowInfo(int type, int data) {
+ mType = type;
+ mData = data;
+ }
+ }
+
+ private static class MultipleDayInfo {
+ final int mPosition;
+ final int mEndDay;
+
+ MultipleDayInfo(int position, int endDay) {
+ mPosition = position;
+ mEndDay = endDay;
+ }
+ }
+
+ /**
+ * Searches for the day that matches the given Time object and returns the
+ * list position of that day. If there are no events for that day, then it
+ * finds the nearest day (before or after) that has events and returns the
+ * list position for that day.
+ *
+ * @param time the date to search for
+ * @return the cursor position of the first event for that date, or zero
+ * if no match was found
+ */
+ public int findDayPositionNearestTime(Time time) {
+ if (mRowInfo == null) {
+ return 0;
+ }
+ long millis = time.toMillis(false /* use isDst */);
+ int julianDay = Time.getJulianDay(millis, time.gmtoff);
+ int minDistance = 1000; // some big number
+ int minIndex = 0;
+ int len = mRowInfo.size();
+ for (int index = 0; index < len; index++) {
+ RowInfo row = mRowInfo.get(index);
+ if (row.mType == TYPE_DAY) {
+ int distance = Math.abs(julianDay - row.mData);
+ if (distance == 0) {
+ return index;
+ }
+ if (distance < minDistance) {
+ minDistance = distance;
+ minIndex = index;
+ }
+ }
+ }
+
+ // We didn't find an exact match so take the nearest day that had
+ // events.
+ return minIndex;
+ }
+
+ /**
+ * Finds the Julian day containing the event at the given position.
+ *
+ * @param position the list position of an event
+ * @return the Julian day containing that event
+ */
+ public int findJulianDayFromPosition(int position) {
+ if (mRowInfo == null || position < 0) {
+ return 0;
+ }
+
+ int len = mRowInfo.size();
+ if (position >= len) return 0; // no row info at this position
+
+ for (int index = position; index >= 0; index--) {
+ RowInfo row = mRowInfo.get(index);
+ if (row.mType == TYPE_DAY) {
+ return row.mData;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Converts a list position to a cursor position. The list contains
+ * day headers as well as events. The cursor contains only events.
+ *
+ * @param listPos the list position of an event
+ * @return the corresponding cursor position of that event
+ */
+ public int getCursorPosition(int listPos) {
+ if (mRowInfo != null && listPos >= 0) {
+ RowInfo row = mRowInfo.get(listPos);
+ if (row.mType == TYPE_MEETING) {
+ return row.mData;
+ }
+ }
+ return listPos;
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ if (mRowInfo != null && position < mRowInfo.size()) {
+ RowInfo row = mRowInfo.get(position);
+ return row.mType == TYPE_MEETING;
+ }
+ return true;
+ }
+}
+
diff --git a/src/com/android/calendar/AlertActivity.java b/src/com/android/calendar/AlertActivity.java
new file mode 100644
index 00000000..913bd85c
--- /dev/null
+++ b/src/com/android/calendar/AlertActivity.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2007 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 static android.provider.Calendar.EVENT_BEGIN_TIME;
+import static android.provider.Calendar.EVENT_END_TIME;
+
+import android.app.Activity;
+import android.app.AlarmManager;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.PixelFormat;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Calendar;
+import android.provider.Calendar.CalendarAlerts;
+import android.provider.Calendar.Events;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.AdapterView.OnItemClickListener;
+
+/**
+ * The alert panel that pops up when there is a calendar event alarm.
+ * This activity is started by an intent that specifies an event id.
+ */
+public class AlertActivity extends Activity {
+
+ // The default snooze delay: 5 minutes
+ public static final long SNOOZE_DELAY = 5 * 60 * 1000L;
+
+ private static final String[] PROJECTION = new String[] {
+ CalendarAlerts._ID, // 0
+ CalendarAlerts.TITLE, // 1
+ CalendarAlerts.EVENT_LOCATION, // 2
+ CalendarAlerts.ALL_DAY, // 3
+ CalendarAlerts.BEGIN, // 4
+ CalendarAlerts.END, // 5
+ CalendarAlerts.EVENT_ID, // 6
+ CalendarAlerts.COLOR, // 7
+ CalendarAlerts.RRULE, // 8
+ CalendarAlerts.HAS_ALARM, // 9
+ CalendarAlerts.STATE, // 10
+ CalendarAlerts.ALARM_TIME, // 11
+ };
+
+ 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_BEGIN = 4;
+ public static final int INDEX_END = 5;
+ public static final int INDEX_EVENT_ID = 6;
+ public static final int INDEX_COLOR = 7;
+ public static final int INDEX_RRULE = 8;
+ public static final int INDEX_HAS_ALARM = 9;
+ public static final int INDEX_STATE = 10;
+ public static final int INDEX_ALARM_TIME = 11;
+
+ // We use one notification id for all events so that we don't clutter
+ // the notification screen. It doesn't matter what the id is, as long
+ // as it is used consistently everywhere.
+ public static final int NOTIFICATION_ID = 0;
+
+ private ContentResolver mResolver;
+ private AlertAdapter mAdapter;
+ private QueryHandler mQueryHandler;
+ private Cursor mCursor;
+ private ListView mListView;
+ private Button mSnoozeAllButton;
+ private Button mDismissAllButton;
+
+ 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()) {
+ mCursor = cursor;
+ mAdapter.changeCursor(cursor);
+
+ // The results are in, enable the buttons
+ mSnoozeAllButton.setEnabled(true);
+ mDismissAllButton.setEnabled(true);
+ } else {
+ cursor.close();
+ }
+ }
+
+ }
+
+ private OnItemClickListener mViewListener = new OnItemClickListener() {
+
+ public void onItemClick(AdapterView parent, View view, int position,
+ long i) {
+ AlertActivity alertActivity = AlertActivity.this;
+ Cursor cursor = alertActivity.getItemForView(view);
+
+ long id = cursor.getInt(AlertActivity.INDEX_EVENT_ID);
+ long startMillis = cursor.getLong(AlertActivity.INDEX_BEGIN);
+ long endMillis = cursor.getLong(AlertActivity.INDEX_END);
+
+ Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.setClass(alertActivity, EventInfoActivity.class);
+ intent.putExtra(EVENT_BEGIN_TIME, startMillis);
+ intent.putExtra(EVENT_END_TIME, endMillis);
+
+ // Mark this alarm as DISMISSED
+ cursor.updateInt(INDEX_STATE, CalendarAlerts.DISMISSED);
+ cursor.commitUpdates();
+
+ startActivity(intent);
+ alertActivity.finish();
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.alert_activity);
+
+ WindowManager.LayoutParams lp = getWindow().getAttributes();
+ lp.width = ViewGroup.LayoutParams.FILL_PARENT;
+ lp.height = ViewGroup.LayoutParams.FILL_PARENT;
+
+ // Get the dim amount from the theme
+ TypedArray a = obtainStyledAttributes(com.android.internal.R.styleable.Theme);
+ lp.dimAmount = a.getFloat(android.R.styleable.Theme_backgroundDimAmount, 0.5f);
+ a.recycle();
+
+ getWindow().setAttributes(lp);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+
+ mResolver = getContentResolver();
+ mQueryHandler = new QueryHandler(mResolver);
+ mAdapter = new AlertAdapter(this, R.layout.alert_item);
+
+ mListView = (ListView) findViewById(R.id.alert_container);
+ mListView.setItemsCanFocus(true);
+ mListView.setAdapter(mAdapter);
+ mListView.setOnItemClickListener(mViewListener);
+
+ mSnoozeAllButton = (Button) findViewById(R.id.snooze_all);
+ mSnoozeAllButton.setOnClickListener(mSnoozeAllListener);
+ mDismissAllButton = (Button) findViewById(R.id.dismiss_all);
+ mDismissAllButton.setOnClickListener(mDismissAllListener);
+
+ // Disable the buttons, since they need mCursor, which is created asynchronously
+ mSnoozeAllButton.setEnabled(false);
+ mDismissAllButton.setEnabled(false);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ // If the cursor is null, start the async handler. If it is not null just requery.
+ if (mCursor == null) {
+ Uri uri = CalendarAlerts.CONTENT_URI_BY_INSTANCE;
+ String selection = CalendarAlerts.STATE + "=" + CalendarAlerts.FIRED;
+ mQueryHandler.startQuery(0, null, uri, PROJECTION, selection,
+ null /* selection args */, CalendarAlerts.DEFAULT_SORT_ORDER);
+ } else {
+ mCursor.requery();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ AlertReceiver.updateAlertNotification(this);
+
+ if (mCursor != null) {
+ mCursor.deactivate();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mCursor != null) {
+ mCursor.close();
+ }
+ }
+
+ private OnClickListener mSnoozeAllListener = new OnClickListener() {
+ public void onClick(View v) {
+ NotificationManager nm =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.cancel(NOTIFICATION_ID);
+ mCursor.moveToPosition(-1);
+ while (mCursor.moveToNext()) {
+ long eventId = mCursor.getLong(INDEX_EVENT_ID);
+ long begin = mCursor.getLong(INDEX_BEGIN);
+ long end = mCursor.getLong(INDEX_END);
+ long alarmTime = mCursor.getLong(INDEX_ALARM_TIME);
+
+ // Mark this alarm as DISMISSED
+ mCursor.updateInt(INDEX_STATE, CalendarAlerts.DISMISSED);
+
+ // Create a new alarm entry in the CalendarAlerts table
+ long now = System.currentTimeMillis();
+ alarmTime = now + SNOOZE_DELAY;
+
+ // Set the "minutes" to zero to indicate this is a snoozed
+ // alarm. There is code in AlertService.java that checks
+ // this field.
+ Uri uri = CalendarAlerts.insert(mResolver, eventId,
+ begin, end, alarmTime, 0 /* minutes */);
+
+ // Set a new alarm to go off after the snooze delay.
+ Intent intent = new Intent(Calendar.EVENT_REMINDER_ACTION);
+ intent.setData(uri);
+ intent.putExtra(Calendar.EVENT_BEGIN_TIME, begin);
+ intent.putExtra(Calendar.EVENT_END_TIME, end);
+
+ PendingIntent sender = PendingIntent.getBroadcast(AlertActivity.this,
+ 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+ Object service = getSystemService(Context.ALARM_SERVICE);
+ AlarmManager alarmManager = (AlarmManager) service;
+ alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, sender);
+ }
+ mCursor.commitUpdates();
+ finish();
+ }
+ };
+
+ private OnClickListener mDismissAllListener = new OnClickListener() {
+ public void onClick(View v) {
+ NotificationManager nm =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.cancel(NOTIFICATION_ID);
+ mCursor.moveToPosition(-1);
+ while (mCursor.moveToNext()) {
+ mCursor.updateInt(INDEX_STATE, CalendarAlerts.DISMISSED);
+ }
+ mCursor.commitUpdates();
+ finish();
+ }
+ };
+
+ public boolean isEmpty() {
+ return (mCursor.getCount() == 0);
+ }
+
+ public Cursor getItemForView(View view) {
+ int index = mListView.getPositionForView(view);
+ if (index < 0) {
+ return null;
+ }
+ return (Cursor) mListView.getAdapter().getItem(index);
+ }
+}
diff --git a/src/com/android/calendar/AlertAdapter.java b/src/com/android/calendar/AlertAdapter.java
new file mode 100644
index 00000000..b182ccd7
--- /dev/null
+++ b/src/com/android/calendar/AlertAdapter.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2008 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.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.PorterDuff;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ResourceCursorAdapter;
+import android.widget.TextView;
+
+public class AlertAdapter extends ResourceCursorAdapter {
+
+ public AlertAdapter(Context context, int resource) {
+ super(context, resource, null);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ TextView textView;
+
+ ImageView stripe = (ImageView) view.findViewById(R.id.vertical_stripe);
+ int color = cursor.getInt(AlertActivity.INDEX_COLOR) & 0xbbffffff;
+ stripe.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
+
+ // Repeating info
+ View repeatContainer = view.findViewById(R.id.repeat_icon);
+ String rrule = cursor.getString(AlertActivity.INDEX_RRULE);
+ if (rrule != null) {
+ repeatContainer.setVisibility(View.VISIBLE);
+ } else {
+ repeatContainer.setVisibility(View.GONE);
+ }
+
+ /*
+ // Reminder
+ boolean hasAlarm = cursor.getInt(AlertActivity.INDEX_HAS_ALARM) != 0;
+ if (hasAlarm) {
+ AgendaAdapter.updateReminder(view, context, cursor.getLong(AlertActivity.INDEX_BEGIN),
+ cursor.getLong(AlertActivity.INDEX_EVENT_ID));
+ }
+ */
+
+ String eventName = cursor.getString(AlertActivity.INDEX_TITLE);
+ String location = cursor.getString(AlertActivity.INDEX_EVENT_LOCATION);
+ long startMillis = cursor.getLong(AlertActivity.INDEX_BEGIN);
+ long endMillis = cursor.getLong(AlertActivity.INDEX_END);
+ boolean allDay = cursor.getInt(AlertActivity.INDEX_ALL_DAY) != 0;
+
+ updateView(context, view, eventName, location, startMillis, endMillis, allDay);
+ }
+
+ public static void updateView(Context context, View view, String eventName, String location,
+ long startMillis, long endMillis, boolean allDay) {
+
+ Resources res = context.getResources();
+ TextView textView;
+
+ // What
+ if (eventName == null || eventName.length() == 0) {
+ eventName = res.getString(R.string.no_title_label);
+ }
+ textView = (TextView) view.findViewById(R.id.event_title);
+ textView.setText(eventName);
+
+ // When
+ String when;
+ int flags;
+ if (allDay) {
+ flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY |
+ DateUtils.FORMAT_SHOW_DATE;
+ } else {
+ flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE;
+ }
+ if (DateFormat.is24HourFormat(context)) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ }
+ when = DateUtils.formatDateRange(context, startMillis, endMillis, flags);
+ textView = (TextView) view.findViewById(R.id.when);
+ textView.setText(when);
+
+ // Where
+ textView = (TextView) view.findViewById(R.id.where);
+ if (location == null || location.length() == 0) {
+ textView.setVisibility(View.GONE);
+ } else {
+ textView.setText(location);
+ }
+ }
+}
diff --git a/src/com/android/calendar/AlertReceiver.java b/src/com/android/calendar/AlertReceiver.java
new file mode 100644
index 00000000..20f065ce
--- /dev/null
+++ b/src/com/android/calendar/AlertReceiver.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2007 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.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.PowerManager;
+import android.preference.PreferenceManager;
+import android.provider.Calendar.CalendarAlerts;
+
+/**
+ * Receives android.intent.action.EVENT_REMINDER intents and handles
+ * event reminders. The intent URI specifies an alert id in the
+ * CalendarAlerts database table. This class also receives the
+ * BOOT_COMPLETED intent so that it can add a status bar notification
+ * if there are Calendar event alarms that have not been dismissed.
+ * It also receives the TIME_CHANGED action so that it can fire off
+ * snoozed alarms that have become ready. The real work is done in
+ * the AlertService class.
+ */
+public class AlertReceiver extends BroadcastReceiver {
+ private static final String[] ALERT_PROJECTION = new String[] {
+ CalendarAlerts.TITLE, // 0
+ CalendarAlerts.EVENT_LOCATION, // 1
+ };
+ private static final int ALERT_INDEX_TITLE = 0;
+ private static final int ALERT_INDEX_EVENT_LOCATION = 1;
+
+ private static final String DELETE_ACTION = "delete";
+
+ private static final String[] PROJECTION = new String[] {
+ CalendarAlerts._ID, // 0
+ CalendarAlerts.STATE, // 1
+ };
+
+ public static final int INDEX_STATE = 1;
+
+ static final Object mStartingServiceSync = new Object();
+ static PowerManager.WakeLock mStartingService;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DELETE_ACTION.equals(intent.getAction())) {
+
+ /* The user has clicked the "Clear All Notifications"
+ * buttons so dismiss all Calendar alerts.
+ */
+ dismissAllEvents(context);
+ } else {
+ Intent i = new Intent();
+ i.setClass(context, AlertService.class);
+ i.putExtras(intent);
+ i.putExtra("action", intent.getAction());
+ Uri uri = intent.getData();
+
+ // This intent might be a BOOT_COMPLETED so it might not have a Uri.
+ if (uri != null) {
+ i.putExtra("uri", uri.toString());
+
+ // Record the received time in the CalendarAlerts table.
+ // This is useful for finding bugs that cause alarms to be
+ // missed or delayed.
+ ContentResolver cr = context.getContentResolver();
+ ContentValues values = new ContentValues();
+ long currentTime = System.currentTimeMillis();
+ values.put(CalendarAlerts.RECEIVED_TIME, currentTime);
+ cr.update(uri, values, null /* where */, null /* args */);
+ }
+ beginStartingService(context, i);
+ }
+ }
+
+ private void dismissAllEvents(Context context) {
+ Uri uri = CalendarAlerts.CONTENT_URI_BY_INSTANCE;
+ String selection = CalendarAlerts.STATE + "=" + CalendarAlerts.FIRED;
+ ContentResolver resolver = context.getContentResolver();
+ Cursor cursor = resolver.query(uri, PROJECTION, selection, null, null);
+ if (cursor != null) {
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ cursor.updateInt(INDEX_STATE, CalendarAlerts.DISMISSED);
+ }
+ cursor.commitUpdates();
+ cursor.close();
+ }
+ }
+
+ /**
+ * Start the service to process the current event notifications, acquiring
+ * the wake lock before returning to ensure that the service will run.
+ */
+ public static void beginStartingService(Context context, Intent intent) {
+ synchronized (mStartingServiceSync) {
+ if (mStartingService == null) {
+ PowerManager pm =
+ (PowerManager)context.getSystemService(Context.POWER_SERVICE);
+ mStartingService = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ "StartingAlertService");
+ mStartingService.setReferenceCounted(false);
+ }
+ mStartingService.acquire();
+ context.startService(intent);
+ }
+ }
+
+ /**
+ * Called back by the service when it has finished processing notifications,
+ * releasing the wake lock if the service is now stopping.
+ */
+ public static void finishStartingService(Service service, int startId) {
+ synchronized (mStartingServiceSync) {
+ if (mStartingService != null) {
+ if (service.stopSelfResult(startId)) {
+ mStartingService.release();
+ }
+ }
+ }
+ }
+
+ public static void updateAlertNotification(Context context) {
+ // This can be called regularly to synchronize the alert notification
+ // with the contents of the CalendarAlerts table.
+
+ ContentResolver cr = context.getContentResolver();
+
+ if (cr == null) {
+ return;
+ }
+
+ String selection = CalendarAlerts.STATE + "=" + CalendarAlerts.FIRED;
+ Cursor alertCursor = CalendarAlerts.query(cr, ALERT_PROJECTION, selection, null);
+
+ NotificationManager nm =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ if (alertCursor == null) {
+ nm.cancel(AlertActivity.NOTIFICATION_ID);
+ return;
+ }
+
+ if (!alertCursor.moveToFirst()) {
+ alertCursor.close();
+ nm.cancel(AlertActivity.NOTIFICATION_ID);
+ return;
+ }
+
+ // Check the settings to see if alerts are disabled
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ String reminderType = prefs.getString(CalendarPreferenceActivity.KEY_ALERTS_TYPE,
+ CalendarPreferenceActivity.ALERT_TYPE_STATUS_BAR);
+ if (reminderType.equals(CalendarPreferenceActivity.ALERT_TYPE_OFF)) {
+ return;
+ }
+
+ String title = alertCursor.getString(ALERT_INDEX_TITLE);
+ String location = alertCursor.getString(ALERT_INDEX_EVENT_LOCATION);
+
+ Notification notification = AlertReceiver.makeNewAlertNotification(context, title,
+ location, alertCursor.getCount());
+ alertCursor.close();
+
+ nm.notify(0, notification);
+ }
+
+ public static Notification makeNewAlertNotification(Context context, String title,
+ String location, int numReminders) {
+ Resources res = context.getResources();
+
+ // Create an intent triggered by clicking on the status icon.
+ Intent clickIntent = new Intent();
+ clickIntent.setClass(context, AlertActivity.class);
+ clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // Create an intent triggered by clicking on the "Clear All Notifications" button
+ Intent deleteIntent = new Intent();
+ deleteIntent.setClass(context, AlertReceiver.class);
+ deleteIntent.setAction(DELETE_ACTION);
+
+ if (title == null || title.length() == 0) {
+ title = res.getString(R.string.no_title_label);
+ }
+
+ String helperString;
+ if (numReminders > 1) {
+ String format;
+ if (numReminders == 2) {
+ format = res.getString(R.string.alert_missed_events_single);
+ } else {
+ format = res.getString(R.string.alert_missed_events_multiple);
+ }
+ helperString = String.format(format, numReminders - 1);
+ } else {
+ helperString = location;
+ }
+
+ Notification notification = new Notification(
+ R.drawable.stat_notify_calendar,
+ null,
+ System.currentTimeMillis());
+ notification.setLatestEventInfo(context,
+ title,
+ helperString,
+ PendingIntent.getActivity(context, 0, clickIntent, 0));
+ notification.deleteIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, 0);
+
+ return notification;
+ }
+}
+
diff --git a/src/com/android/calendar/AlertService.java b/src/com/android/calendar/AlertService.java
new file mode 100644
index 00000000..eb705046
--- /dev/null
+++ b/src/com/android/calendar/AlertService.java
@@ -0,0 +1,437 @@
+/*
+ * Copyright (C) 2008 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.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.preference.PreferenceManager;
+import android.provider.Calendar;
+import android.provider.Calendar.Attendees;
+import android.provider.Calendar.CalendarAlerts;
+import android.provider.Calendar.Instances;
+import android.provider.Calendar.Reminders;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+
+/**
+ * This service is used to handle calendar event reminders.
+ */
+public class AlertService extends Service {
+ private static final String TAG = "AlertService";
+
+ private volatile Looper mServiceLooper;
+ private volatile ServiceHandler mServiceHandler;
+
+ private static final String[] ALERT_PROJECTION = new String[] {
+ CalendarAlerts._ID, // 0
+ CalendarAlerts.EVENT_ID, // 1
+ CalendarAlerts.STATE, // 2
+ CalendarAlerts.TITLE, // 3
+ CalendarAlerts.EVENT_LOCATION, // 4
+ CalendarAlerts.SELF_ATTENDEE_STATUS, // 5
+ CalendarAlerts.ALL_DAY, // 6
+ CalendarAlerts.ALARM_TIME, // 7
+ CalendarAlerts.MINUTES, // 8
+ CalendarAlerts.BEGIN, // 9
+ };
+
+ // We just need a simple projection that returns any column
+ private static final String[] ALERT_PROJECTION_SMALL = new String[] {
+ CalendarAlerts._ID, // 0
+ };
+
+ private static final int ALERT_INDEX_ID = 0;
+ private static final int ALERT_INDEX_EVENT_ID = 1;
+ private static final int ALERT_INDEX_STATE = 2;
+ private static final int ALERT_INDEX_TITLE = 3;
+ private static final int ALERT_INDEX_EVENT_LOCATION = 4;
+ private static final int ALERT_INDEX_SELF_ATTENDEE_STATUS = 5;
+ private static final int ALERT_INDEX_ALL_DAY = 6;
+ private static final int ALERT_INDEX_ALARM_TIME = 7;
+ private static final int ALERT_INDEX_MINUTES = 8;
+ private static final int ALERT_INDEX_BEGIN = 9;
+
+ private String[] INSTANCE_PROJECTION = { Instances.BEGIN, Instances.END };
+ private static final int INSTANCES_INDEX_BEGIN = 0;
+ private static final int INSTANCES_INDEX_END = 1;
+
+ // We just need a simple projection that returns any column
+ private static final String[] REMINDER_PROJECTION_SMALL = new String[] {
+ Reminders._ID, // 0
+ };
+
+ private void processMessage(Message msg) {
+ Bundle bundle = (Bundle) msg.obj;
+
+ // On reboot, update the notification bar with the contents of the
+ // CalendarAlerts table.
+ String action = bundle.getString("action");
+ if (action.equals(Intent.ACTION_BOOT_COMPLETED)
+ || action.equals(Intent.ACTION_TIME_CHANGED)) {
+ doTimeChanged();
+ return;
+ }
+
+ // The Uri specifies an entry in the CalendarAlerts table
+ Uri alertUri = Uri.parse(bundle.getString("uri"));
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "uri: " + alertUri);
+ }
+
+ ContentResolver cr = getContentResolver();
+ Cursor alertCursor = cr.query(alertUri, ALERT_PROJECTION,
+ null /* selection */, null, null /* sort order */);
+
+ long alertId, eventId, instanceId, alarmTime;
+ int minutes;
+ String eventName;
+ String location;
+ boolean allDay;
+ boolean declined = false;
+ try {
+ if (alertCursor == null || !alertCursor.moveToFirst()) {
+ // This can happen if the event was deleted.
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "alert not found");
+ }
+ return;
+ }
+ alertId = alertCursor.getLong(ALERT_INDEX_ID);
+ eventId = alertCursor.getLong(ALERT_INDEX_EVENT_ID);
+ minutes = alertCursor.getInt(ALERT_INDEX_MINUTES);
+ eventName = alertCursor.getString(ALERT_INDEX_TITLE);
+ location = alertCursor.getString(ALERT_INDEX_EVENT_LOCATION);
+ allDay = alertCursor.getInt(ALERT_INDEX_ALL_DAY) != 0;
+ alarmTime = alertCursor.getLong(ALERT_INDEX_ALARM_TIME);
+ declined = alertCursor.getInt(ALERT_INDEX_SELF_ATTENDEE_STATUS) ==
+ Attendees.ATTENDEE_STATUS_DECLINED;
+
+ // If the event was declined, then mark the alarm DISMISSED,
+ // otherwise, mark the alarm FIRED.
+ int newState = CalendarAlerts.FIRED;
+ if (declined) {
+ newState = CalendarAlerts.DISMISSED;
+ }
+ alertCursor.updateInt(ALERT_INDEX_STATE, newState);
+ alertCursor.commitUpdates();
+ } finally {
+ if (alertCursor != null) {
+ alertCursor.close();
+ }
+ }
+
+ // Do not show an alert if the event was declined
+ if (declined) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "event declined, alert cancelled");
+ }
+ return;
+ }
+
+ long beginTime = bundle.getLong(Calendar.EVENT_BEGIN_TIME, 0);
+ long endTime = bundle.getLong(Calendar.EVENT_END_TIME, 0);
+
+ // Check if this alarm is still valid. The time of the event may
+ // have been changed, or the reminder may have been changed since
+ // this alarm was set. First, search for an instance in the Instances
+ // that has the same event id and the same begin and end time.
+ // Then check for a reminder in the Reminders table to ensure that
+ // the reminder minutes is consistent with this alarm.
+ String selection = Instances.EVENT_ID + "=" + eventId;
+ Cursor instanceCursor = Instances.query(cr, INSTANCE_PROJECTION,
+ beginTime, endTime, selection, Instances.DEFAULT_SORT_ORDER);
+ long instanceBegin = 0, instanceEnd = 0;
+ try {
+ if (instanceCursor == null || !instanceCursor.moveToFirst()) {
+ // Delete this alarm from the CalendarAlerts table
+ cr.delete(alertUri, null /* selection */, null /* selection args */);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "instance not found, alert cancelled");
+ }
+ return;
+ }
+ instanceBegin = instanceCursor.getLong(INSTANCES_INDEX_BEGIN);
+ instanceEnd = instanceCursor.getLong(INSTANCES_INDEX_END);
+ } finally {
+ if (instanceCursor != null) {
+ instanceCursor.close();
+ }
+ }
+
+ // Check that a reminder for this event exists with the same number
+ // of minutes. But snoozed alarms have minutes = 0, so don't do this
+ // check for snoozed alarms.
+ if (minutes > 0) {
+ selection = Reminders.EVENT_ID + "=" + eventId
+ + " AND " + Reminders.MINUTES + "=" + minutes;
+ Cursor reminderCursor = cr.query(Reminders.CONTENT_URI, REMINDER_PROJECTION_SMALL,
+ selection, null /* selection args */, null /* sort order */);
+ try {
+ if (reminderCursor == null || reminderCursor.getCount() == 0) {
+ // Delete this alarm from the CalendarAlerts table
+ cr.delete(alertUri, null /* selection */, null /* selection args */);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "reminder not found, alert cancelled");
+ }
+ return;
+ }
+ } finally {
+ if (reminderCursor != null) {
+ reminderCursor.close();
+ }
+ }
+ }
+
+ // If the event time was changed and the event has already ended,
+ // then don't sound the alarm.
+ if (alarmTime > instanceEnd) {
+ // Delete this alarm from the CalendarAlerts table
+ cr.delete(alertUri, null /* selection */, null /* selection args */);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "event ended, alert cancelled");
+ }
+ return;
+ }
+
+ // If minutes > 0, then this is a normal alarm (not a snoozed alarm)
+ // so check for duplicate alarms. A duplicate alarm can occur when
+ // the start time of an event is changed to an earlier time. The
+ // later alarm (that was first scheduled for the later event time)
+ // should be discarded.
+ long computedAlarmTime = instanceBegin - minutes * DateUtils.MINUTE_IN_MILLIS;
+ if (minutes > 0 && computedAlarmTime != alarmTime) {
+ // If the event time was changed to a later time, then the computed
+ // alarm time is in the future and we shouldn't sound this alarm.
+ if (computedAlarmTime > alarmTime) {
+ // Delete this alarm from the CalendarAlerts table
+ cr.delete(alertUri, null /* selection */, null /* selection args */);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "event postponed, alert cancelled");
+ }
+ return;
+ }
+
+ // Check for another alarm in the CalendarAlerts table that has the
+ // same event id and the same "minutes". This can occur
+ // if the event start time was changed to an earlier time and the
+ // alarm for the later time goes off. To avoid discarding alarms
+ // for repeating events (that have the same event id), we check
+ // that the other alarm fired recently (within an hour of this one).
+ long recently = alarmTime - 60 * DateUtils.MINUTE_IN_MILLIS;
+ selection = CalendarAlerts.EVENT_ID + "=" + eventId
+ + " AND " + CalendarAlerts.TABLE_NAME + "." + CalendarAlerts._ID
+ + "!=" + alertId
+ + " AND " + CalendarAlerts.MINUTES + "=" + minutes
+ + " AND " + CalendarAlerts.ALARM_TIME + ">" + recently
+ + " AND " + CalendarAlerts.ALARM_TIME + "<=" + alarmTime;
+ alertCursor = CalendarAlerts.query(cr, ALERT_PROJECTION_SMALL, selection, null);
+ if (alertCursor != null) {
+ try {
+ if (alertCursor.getCount() > 0) {
+ // Delete this alarm from the CalendarAlerts table
+ cr.delete(alertUri, null /* selection */, null /* selection args */);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "duplicate alarm, alert cancelled");
+ }
+ return;
+ }
+ } finally {
+ alertCursor.close();
+ }
+ }
+ }
+
+ // Find all the alerts that have fired but have not been dismissed
+ selection = CalendarAlerts.STATE + "=" + CalendarAlerts.FIRED;
+ alertCursor = CalendarAlerts.query(cr, ALERT_PROJECTION, selection, null);
+
+ if (alertCursor == null || alertCursor.getCount() == 0) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "no fired alarms found");
+ }
+ return;
+ }
+
+ int numReminders = alertCursor.getCount();
+ try {
+ while (alertCursor.moveToNext()) {
+ long otherEventId = alertCursor.getLong(ALERT_INDEX_EVENT_ID);
+ long otherAlertId = alertCursor.getLong(ALERT_INDEX_ID);
+ int otherAlarmState = alertCursor.getInt(ALERT_INDEX_STATE);
+ long otherBeginTime = alertCursor.getLong(ALERT_INDEX_BEGIN);
+ if (otherEventId == eventId && otherAlertId != alertId
+ && otherAlarmState == CalendarAlerts.FIRED
+ && otherBeginTime == beginTime) {
+ // This event already has an alert that fired and has not
+ // been dismissed. This can happen if an event has
+ // multiple reminders. Do not count this as a separate
+ // reminder. But we do want to sound the alarm and vibrate
+ // the phone, if necessary.
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "multiple alarms for this event");
+ }
+ numReminders -= 1;
+ }
+ }
+ } finally {
+ alertCursor.close();
+ }
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "creating new alarm notification, numReminders: " + numReminders);
+ }
+ Notification notification = AlertReceiver.makeNewAlertNotification(this, eventName,
+ location, numReminders);
+
+ // Generate either a pop-up dialog, status bar notification, or
+ // neither. Pop-up dialog and status bar notification may include a
+ // sound, an alert, or both. A status bar notification also includes
+ // a toast.
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ String reminderType = prefs.getString(CalendarPreferenceActivity.KEY_ALERTS_TYPE,
+ CalendarPreferenceActivity.ALERT_TYPE_STATUS_BAR);
+
+ if (reminderType.equals(CalendarPreferenceActivity.ALERT_TYPE_OFF)) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "alert preference is OFF");
+ }
+ return;
+ }
+
+ NotificationManager nm =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ boolean reminderVibrate =
+ prefs.getBoolean(CalendarPreferenceActivity.KEY_ALERTS_VIBRATE, false);
+ String reminderRingtone =
+ prefs.getString(CalendarPreferenceActivity.KEY_ALERTS_RINGTONE, null);
+
+ // Possibly generate a vibration
+ if (reminderVibrate) {
+ notification.defaults |= Notification.DEFAULT_VIBRATE;
+ }
+
+ // Possibly generate a sound. If 'Silent' is chosen, the ringtone string will be empty.
+ notification.sound = TextUtils.isEmpty(reminderRingtone) ? null : Uri
+ .parse(reminderRingtone);
+
+ if (reminderType.equals(CalendarPreferenceActivity.ALERT_TYPE_ALERTS)) {
+ Intent alertIntent = new Intent();
+ alertIntent.setClass(this, AlertActivity.class);
+ alertIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(alertIntent);
+ } else {
+ LayoutInflater inflater;
+ inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.alert_toast, null);
+
+ AlertAdapter.updateView(this, view, eventName, location, beginTime, endTime, allDay);
+ }
+
+ // Record the notify time in the CalendarAlerts table.
+ // This is used for debugging missed alarms.
+ ContentValues values = new ContentValues();
+ long currentTime = System.currentTimeMillis();
+ values.put(CalendarAlerts.NOTIFY_TIME, currentTime);
+ cr.update(alertUri, values, null /* where */, null /* args */);
+
+ // The notification time should be pretty close to the reminder time
+ // that the user set for this event. If the notification is late, then
+ // that's a bug and we should log an error.
+ if (currentTime > alarmTime + DateUtils.MINUTE_IN_MILLIS) {
+ long minutesLate = (currentTime - alarmTime) / DateUtils.MINUTE_IN_MILLIS;
+ int flags = DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_TIME;
+ String alarmTimeStr = DateUtils.formatDateTime(this, alarmTime, flags);
+ String currentTimeStr = DateUtils.formatDateTime(this, currentTime, flags);
+ Log.w(TAG, "Calendar reminder alarm for event id " + eventId
+ + " is " + minutesLate + " minute(s) late;"
+ + " expected alarm at: " + alarmTimeStr
+ + " but got it at: " + currentTimeStr);
+ }
+
+ nm.notify(0, notification);
+ }
+
+ private void doTimeChanged() {
+ ContentResolver cr = getContentResolver();
+ Object service = getSystemService(Context.ALARM_SERVICE);
+ AlarmManager manager = (AlarmManager) service;
+ CalendarAlerts.rescheduleMissedAlarms(cr, this, manager);
+ AlertReceiver.updateAlertNotification(this);
+ }
+
+ private final class ServiceHandler extends Handler {
+ public ServiceHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ processMessage(msg);
+ // NOTE: We MUST not call stopSelf() directly, since we need to
+ // make sure the wake lock acquired by AlertReceiver is released.
+ AlertReceiver.finishStartingService(AlertService.this, msg.arg1);
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ HandlerThread thread = new HandlerThread("AlertService",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ thread.start();
+
+ mServiceLooper = thread.getLooper();
+ mServiceHandler = new ServiceHandler(mServiceLooper);
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ Message msg = mServiceHandler.obtainMessage();
+ msg.arg1 = startId;
+ msg.obj = intent.getExtras();
+ mServiceHandler.sendMessage(msg);
+ }
+
+ @Override
+ public void onDestroy() {
+ mServiceLooper.quit();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+}
diff --git a/src/com/android/calendar/CalendarActivity.java b/src/com/android/calendar/CalendarActivity.java
new file mode 100644
index 00000000..639d59c7
--- /dev/null
+++ b/src/com/android/calendar/CalendarActivity.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2007 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 dalvik.system.VMRuntime;
+
+import android.accounts.AccountMonitor;
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.Calendar;
+import android.text.format.Time;
+import android.view.GestureDetector;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.ProgressBar;
+import android.widget.ViewSwitcher;
+
+/**
+ * This is the base class for Day and Week Activities.
+ */
+public class CalendarActivity extends Activity implements Navigator {
+
+ private static final long INITIAL_HEAP_SIZE = 4*1024*1024;
+
+ protected static final String BUNDLE_KEY_RESTORE_TIME = "key_restore_time";
+
+ private ContentResolver mContentResolver;
+
+ private AccountMonitor mAccountMonitor;
+
+ protected ProgressBar mProgressBar;
+ protected ViewSwitcher mViewSwitcher;
+ protected Animation mInAnimationForward;
+ protected Animation mOutAnimationForward;
+ protected Animation mInAnimationBackward;
+ protected Animation mOutAnimationBackward;
+ EventLoader mEventLoader;
+
+ Time mSelectedDay = new Time();
+
+ /* package */ GestureDetector mGestureDetector;
+
+ /**
+ * Listens for intent broadcasts
+ */
+ private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_TIME_CHANGED)
+ || action.equals(Intent.ACTION_DATE_CHANGED)
+ || action.equals(Intent.ACTION_TIMEZONE_CHANGED)) {
+ eventsChanged();
+ }
+ }
+ };
+
+ // Create an observer so that we can update the views whenever a
+ // Calendar event changes.
+ private ContentObserver mObserver = new ContentObserver(new Handler())
+ {
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ eventsChanged();
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ // Eliminate extra GCs during startup by setting the initial heap size to 4MB.
+ // TODO: We should restore the old heap size once the activity reaches the idle state
+ long oldHeapSize = VMRuntime.getRuntime().setMinimumHeapSize(INITIAL_HEAP_SIZE);
+
+ setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);
+ mContentResolver = getContentResolver();
+
+ mInAnimationForward = AnimationUtils.loadAnimation(this, R.anim.slide_left_in);
+ mOutAnimationForward = AnimationUtils.loadAnimation(this, R.anim.slide_left_out);
+ mInAnimationBackward = AnimationUtils.loadAnimation(this, R.anim.slide_right_in);
+ mOutAnimationBackward = AnimationUtils.loadAnimation(this, R.anim.slide_right_out);
+
+ mGestureDetector = new GestureDetector(this, new CalendarGestureListener());
+ mEventLoader = new EventLoader(this);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ Time time = new Time();
+ time.set(savedInstanceState.getLong(BUNDLE_KEY_RESTORE_TIME));
+ view.setSelectedDay(time);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mEventLoader.startBackgroundThread();
+ eventsChanged();
+
+ // Register for Intent broadcasts
+ IntentFilter filter = new IntentFilter();
+
+ filter.addAction(Intent.ACTION_TIME_CHANGED);
+ filter.addAction(Intent.ACTION_DATE_CHANGED);
+ filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+ registerReceiver(mIntentReceiver, filter);
+
+ mContentResolver.registerContentObserver(Calendar.Events.CONTENT_URI,
+ true, mObserver);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putLong(BUNDLE_KEY_RESTORE_TIME, getSelectedTimeInMillis());
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (mAccountMonitor != null) {
+ mAccountMonitor.close();
+ }
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mContentResolver.unregisterContentObserver(mObserver);
+ unregisterReceiver(mIntentReceiver);
+
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ view.cleanup();
+ view = (CalendarView) mViewSwitcher.getNextView();
+ view.cleanup();
+ mEventLoader.stopBackgroundThread();
+ }
+
+ void startProgressSpinner() {
+ // start the progress spinner
+ mProgressBar.setVisibility(View.VISIBLE);
+ }
+
+ void stopProgressSpinner() {
+ // stop the progress spinner
+ mProgressBar.setVisibility(View.GONE);
+ }
+
+ /* Navigator interface methods */
+ public void goTo(Time time) {
+ CalendarView current = (CalendarView) mViewSwitcher.getCurrentView();
+
+ if (current.getSelectedTime().before(time)) {
+ mViewSwitcher.setInAnimation(mInAnimationForward);
+ mViewSwitcher.setOutAnimation(mOutAnimationForward);
+ } else {
+ mViewSwitcher.setInAnimation(mInAnimationBackward);
+ mViewSwitcher.setOutAnimation(mOutAnimationBackward);
+ }
+
+ CalendarView next = (CalendarView) mViewSwitcher.getNextView();
+ next.setSelectedDay(time);
+ next.reloadEvents();
+ mViewSwitcher.showNext();
+ next.requestFocus();
+ }
+
+ /**
+ * Returns the selected time in milliseconds. The milliseconds are measured
+ * in UTC milliseconds from the epoch and uniquely specifies any selectable
+ * time.
+ *
+ * @return the selected time in milliseconds
+ */
+ public long getSelectedTimeInMillis() {
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ return view.getSelectedTimeInMillis();
+ }
+
+ public long getSelectedTime() {
+ return getSelectedTimeInMillis();
+ }
+
+ public void goToToday() {
+ mSelectedDay.set(System.currentTimeMillis());
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ view.setSelectedDay(mSelectedDay);
+ view.reloadEvents();
+ }
+
+ public boolean getAllDay() {
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ return view.mSelectionAllDay;
+ }
+
+ void eventsChanged() {
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ view.clearCachedEvents();
+ view.reloadEvents();
+ }
+
+ Event getSelectedEvent() {
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ return view.getSelectedEvent();
+ }
+
+ boolean isEventSelected() {
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ return view.isEventSelected();
+ }
+
+ Event getNewEvent() {
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ return view.getNewEvent();
+ }
+
+ public CalendarView getNextView() {
+ return (CalendarView) mViewSwitcher.getNextView();
+ }
+
+ public View switchViews(boolean forward, float xOffSet, float width) {
+ long offset = 0;
+ if (xOffSet != 0) {
+
+ // The user might have scrolled the view to the left or right
+ // in which case we just want to animate the bit left over
+ // instead of animating all of it. So calculate how much
+ // it's been moved already and animate the remaining portion
+ double progress = ((width - (Math.abs(xOffSet))) / width);
+ long duration = mInAnimationForward.getDuration();
+ offset = -1 * (long) (duration - (duration * progress));
+ }
+ if (forward) {
+ mInAnimationForward.setStartOffset(offset);
+ mOutAnimationForward.setStartOffset(offset);
+ mViewSwitcher.setInAnimation(mInAnimationForward);
+ mViewSwitcher.setOutAnimation(mOutAnimationForward);
+ } else {
+ mInAnimationBackward.setStartOffset(offset);
+ mOutAnimationBackward.setStartOffset(offset);
+ mViewSwitcher.setInAnimation(mInAnimationBackward);
+ mViewSwitcher.setOutAnimation(mOutAnimationBackward);
+ }
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ view.cleanup();
+ mViewSwitcher.showNext();
+ view = (CalendarView) mViewSwitcher.getCurrentView();
+ view.requestFocus();
+ view.reloadEvents();
+ return view;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ MenuHelper.onPrepareOptionsMenu(this, menu);
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (! MenuHelper.onCreateOptionsMenu(menu)) {
+ return false;
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (MenuHelper.onOptionsItemSelected(this, item, this)) {
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mGestureDetector.onTouchEvent(ev)) {
+ return true;
+ }
+ return super.onTouchEvent(ev);
+ }
+
+ class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener {
+ @Override
+ public boolean onSingleTapUp(MotionEvent ev) {
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ view.doSingleTapUp(ev);
+ return true;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent ev) {
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ view.doShowPress(ev);
+ }
+
+ @Override
+ public void onLongPress(MotionEvent ev) {
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ view.doLongPress(ev);
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ view.doScroll(e1, e2, distanceX, distanceY);
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ view.doFling(e1, e2, velocityX, velocityY);
+ return true;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent ev) {
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ view.doDown(ev);
+ return true;
+ }
+ }
+}
+
diff --git a/src/com/android/calendar/CalendarApplication.java b/src/com/android/calendar/CalendarApplication.java
new file mode 100644
index 00000000..a3dad951
--- /dev/null
+++ b/src/com/android/calendar/CalendarApplication.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2007 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.app.Application;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+public class CalendarApplication extends Application {
+
+ // TODO: get rid of this global member.
+ public Event currentEvent = null;
+
+ /**
+ * The Screen class defines a node in a linked list. This list contains
+ * the screens that were visited, with the more recently visited screens
+ * coming earlier in the list. The "next" pointer of the head node
+ * points to the first element in the list (the most recently visited
+ * screen).
+ */
+ /* package */ class Screen {
+ public int id;
+ public Screen next;
+ public Screen previous;
+
+ public Screen(int id) {
+ this.id = id;
+ next = this;
+ previous = this;
+ }
+
+ // Adds the given node to the list after this one
+ public void insert(Screen node) {
+ node.next = next;
+ node.previous = this;
+ next.previous = node;
+ next = node;
+ }
+
+ // Removes this node from the list it is in.
+ public void unlink() {
+ next.previous = previous;
+ previous.next = next;
+ }
+ }
+
+ public static final int MONTH_VIEW_ID = 0;
+ public static final int WEEK_VIEW_ID = 1;
+ public static final int DAY_VIEW_ID = 2;
+ public static final int AGENDA_VIEW_ID = 3;
+
+ public static final String[] ACTIVITY_NAMES = new String[] {
+ MonthActivity.class.getName(),
+ WeekActivity.class.getName(),
+ DayActivity.class.getName(),
+ AgendaActivity.class.getName(),
+ };
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ /*
+ * Ensure the default values are set for any receiver, activity,
+ * service, etc. of Calendar
+ */
+ PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
+ }
+
+}
diff --git a/src/com/android/calendar/CalendarData.java b/src/com/android/calendar/CalendarData.java
new file mode 100644
index 00000000..49f3056f
--- /dev/null
+++ b/src/com/android/calendar/CalendarData.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2006 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;
+
+public final class CalendarData {
+ static final String[] sDateStrings = { "0", "1", "2", "3", "4", "5", "6",
+ "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17",
+ "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28",
+ "29", "30", "31" };
+
+ static final String[] sMonthNumStrings = { "1", "2", "3", "4", "5", "6",
+ "7", "8", "9", "10", "11", "12" };
+
+ static final String[] s12Hours = { "12 AM", "1 AM", "2 AM", "3 AM", "4 AM",
+ "5 AM", "6 AM", "7 AM", "8 AM", "9 AM", "10 AM", "11 AM", "Noon",
+ "1 PM", "2 PM", "3 PM", "4 PM", "5 PM", "6 PM", "7 PM", "8 PM",
+ "9 PM", "10 PM", "11 PM", "12 AM" };
+
+ static final String[] s12AmPm = { "AM", "AM", "AM", "AM", "AM",
+ "AM", "AM", "AM", "AM", "AM", "AM", "AM", "PM",
+ "PM", "PM", "PM", "PM", "PM", "PM", "PM", "PM",
+ "PM", "PM", "PM", "AM" };
+
+ static final String[] s12HoursNoAmPm = { "12", "1", "2", "3", "4",
+ "5", "6", "7", "8", "9", "10", "11", "12",
+ "1", "2", "3", "4", "5", "6", "7", "8",
+ "9", "10", "11", "12" };
+
+ static final String[] s24Hours = { "00", "01", "02", "03", "04", "05",
+ "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16",
+ "17", "18", "19", "20", "21", "22", "23", "00" };
+
+ static final String[] sMinutes = {
+ ":00", ":01", ":02", ":03", ":04", ":05", ":06", ":07", ":08", ":09",
+ ":10", ":11", ":12", ":13", ":14", ":15", ":16", ":17", ":18", ":19",
+ ":20", ":21", ":22", ":23", ":24", ":25", ":26", ":27", ":28", ":29",
+ ":30", ":31", ":32", ":33", ":34", ":35", ":36", ":37", ":38", ":39",
+ ":40", ":41", ":42", ":43", ":44", ":45", ":46", ":47", ":48", ":49",
+ ":50", ":51", ":52", ":53", ":54", ":55", ":56", ":57", ":58", ":59"
+ };
+}
diff --git a/src/com/android/calendar/CalendarPreferenceActivity.java b/src/com/android/calendar/CalendarPreferenceActivity.java
new file mode 100644
index 00000000..78d60651
--- /dev/null
+++ b/src/com/android/calendar/CalendarPreferenceActivity.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2007 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.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
+import android.preference.CheckBoxPreference;
+import android.preference.RingtonePreference;
+
+public class CalendarPreferenceActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener {
+ // Preference keys
+ static final String KEY_HIDE_DECLINED = "preferences_hide_declined";
+ static final String KEY_ALERTS_TYPE = "preferences_alerts_type";
+ static final String KEY_ALERTS_VIBRATE = "preferences_alerts_vibrate";
+ static final String KEY_ALERTS_RINGTONE = "preferences_alerts_ringtone";
+ static final String KEY_DEFAULT_REMINDER = "preferences_default_reminder";
+ static final String KEY_START_VIEW = "startView";
+ static final String KEY_DETAILED_VIEW = "preferredDetailedView";
+
+ // These must be in sync with the array preferences_alert_type_values
+ static final String ALERT_TYPE_ALERTS = "0";
+ static final String ALERT_TYPE_STATUS_BAR = "1";
+ static final String ALERT_TYPE_OFF = "2";
+
+ // Default preference values
+ static final String DEFAULT_START_VIEW =
+ CalendarApplication.ACTIVITY_NAMES[CalendarApplication.MONTH_VIEW_ID];
+ static final String DEFAULT_DETAILED_VIEW =
+ CalendarApplication.ACTIVITY_NAMES[CalendarApplication.DAY_VIEW_ID];
+
+ ListPreference mAlertType;
+ CheckBoxPreference mVibrate;
+ RingtonePreference mRingtone;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.preferences);
+
+ PreferenceScreen preferenceScreen = getPreferenceScreen();
+ preferenceScreen.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
+ mAlertType = (ListPreference) preferenceScreen.findPreference(KEY_ALERTS_TYPE);
+ mVibrate = (CheckBoxPreference) preferenceScreen.findPreference(KEY_ALERTS_VIBRATE);
+ mRingtone = (RingtonePreference) preferenceScreen.findPreference(KEY_ALERTS_RINGTONE);
+
+ updateChildPreferences();
+ }
+
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (key.equals(KEY_ALERTS_TYPE)) {
+ updateChildPreferences();
+ }
+ }
+
+ private void updateChildPreferences() {
+ if (mAlertType.getValue().equals(ALERT_TYPE_OFF)) {
+ mVibrate.setChecked(false);
+ mVibrate.setEnabled(false);
+ mRingtone.setEnabled(false);
+ } else {
+ mVibrate.setEnabled(true);
+ mRingtone.setEnabled(true);
+ }
+ }
+}
diff --git a/src/com/android/calendar/CalendarView.java b/src/com/android/calendar/CalendarView.java
new file mode 100644
index 00000000..a50c44be
--- /dev/null
+++ b/src/com/android/calendar/CalendarView.java
@@ -0,0 +1,2970 @@
+/*
+ * Copyright (C) 2007 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 static android.provider.Calendar.EVENT_BEGIN_TIME;
+import static android.provider.Calendar.EVENT_END_TIME;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.Paint.Style;
+import android.graphics.Path.Direction;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Calendar.Attendees;
+import android.provider.Calendar.Calendars;
+import android.provider.Calendar.Events;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.ImageView;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+
+/**
+ * This is the base class for a set of classes that implement views (day view
+ * and week view to start with) that share some common code.
+ */
+public class CalendarView extends View
+ implements View.OnCreateContextMenuListener, View.OnClickListener {
+
+ private boolean mOnFlingCalled;
+
+ protected CalendarApplication mCalendarApp;
+ protected CalendarActivity mParentActivity;
+
+ private static final String[] CALENDARS_PROJECTION = new String[] {
+ Calendars._ID, // 0
+ Calendars.ACCESS_LEVEL, // 1
+ };
+ private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1;
+ private static final String CALENDARS_WHERE = Calendars._ID + "=%d";
+
+ private static final String[] ATTENDEES_PROJECTION = new String[] {
+ Attendees._ID, // 0
+ Attendees.ATTENDEE_RELATIONSHIP, // 1
+ };
+ private static final int ATTENDEES_INDEX_RELATIONSHIP = 1;
+ private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d";
+
+ private static final float SMALL_ROUND_RADIUS = 3.0F;
+
+ private static final int FROM_NONE = 0;
+ private static final int FROM_ABOVE = 1;
+ private static final int FROM_BELOW = 2;
+ private static final int FROM_LEFT = 4;
+ private static final int FROM_RIGHT = 8;
+
+ private static final int HORIZONTAL_SCROLL_THRESHOLD = 50;
+
+ private ContinueScroll mContinueScroll = new ContinueScroll();
+
+ // Make this visible within the package for more informative debugging
+ Time mBaseDate;
+
+ private Typeface mBold = Typeface.DEFAULT_BOLD;
+ private int mFirstJulianDay;
+ private int mLastJulianDay;
+
+ private int mMonthLength;
+ private int mFirstDate;
+ private int[] mEarliestStartHour; // indexed by the week day offset
+ private boolean[] mHasAllDayEvent; // indexed by the week day offset
+
+ private String mDetailedView = CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW;
+
+ /**
+ * This variable helps to avoid unnecessarily reloading events by keeping
+ * track of the start millis parameter used for the most recent loading
+ * of events. If the next reload matches this, then the events are not
+ * reloaded. To force a reload, set this to zero (this is set to zero
+ * in the method clearCachedEvents()).
+ */
+ private long mLastReloadMillis;
+
+ private ArrayList<Event> mEvents = new ArrayList<Event>();
+ private int mSelectionDay; // Julian day
+ private int mSelectionHour;
+
+ /* package private so that CalendarActivity can read it when creating new
+ * events
+ */
+ boolean mSelectionAllDay;
+
+ private int mCellWidth;
+ private boolean mLaunchNewView;
+
+ // Pre-allocate these objects and re-use them
+ private Rect mRect = new Rect();
+ private RectF mRectF = new RectF();
+ private Rect mSrcRect = new Rect();
+ private Rect mDestRect = new Rect();
+ private Paint mPaint = new Paint();
+ private Paint mEventTextPaint = new Paint();
+ private Paint mSelectionPaint = new Paint();
+ private Path mPath = new Path();
+
+ protected boolean mDrawTextInEventRect;
+ private int mStartDay;
+
+ private PopupWindow mPopup;
+ private View mPopupView;
+
+ // The number of milliseconds to show the popup window
+ private static final int POPUP_DISMISS_DELAY = 3000;
+ private DismissPopup mDismissPopup = new DismissPopup();
+
+ // For drawing to an off-screen Canvas
+ private Bitmap mBitmap;
+ private Canvas mCanvas;
+ private boolean mRedrawScreen = true;
+ private boolean mRemeasure = true;
+
+ private final EventLoader mEventLoader;
+ protected final EventGeometry mEventGeometry;
+
+ private static final int DAY_GAP = 1;
+ private static final int HOUR_GAP = 1;
+ private static final int SINGLE_ALLDAY_HEIGHT = 20;
+ private static final int MAX_ALLDAY_HEIGHT = 72;
+ private static final int ALLDAY_TOP_MARGIN = 3;
+ private static final int MAX_ALLDAY_EVENT_HEIGHT = 18;
+
+ /* The extra space to leave above the text in all-day events */
+ private static final int ALL_DAY_TEXT_TOP_MARGIN = 0;
+
+ /* The extra space to leave above the text in normal events */
+ private static final int NORMAL_TEXT_TOP_MARGIN = 2;
+
+ private static final int HOURS_LEFT_MARGIN = 2;
+ private static final int HOURS_RIGHT_MARGIN = 4;
+ private static final int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN;
+
+ /* package */ static final int MINUTES_PER_HOUR = 60;
+ /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24;
+ /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000;
+ /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000);
+ /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24;
+
+ private static final int NORMAL_FONT_SIZE = 12;
+ private static final int EVENT_TEXT_FONT_SIZE = 12;
+ private static final int HOURS_FONT_SIZE = 12;
+ private static final int AMPM_FONT_SIZE = 9;
+ private static final int MIN_CELL_WIDTH_FOR_TEXT = 10;
+ private static final int MAX_EVENT_TEXT_LEN = 500;
+ private static final float MIN_EVENT_HEIGHT = 15.0F; // in pixels
+
+ private static int mSelectionColor;
+ private static int mPressedColor;
+ private static int mSelectedEventTextColor;
+ private static int mEventTextColor;
+
+ private int mViewStartX;
+ private int mViewStartY;
+ private int mMaxViewStartY;
+ private int mBitmapHeight;
+ private int mViewHeight;
+ private int mViewWidth;
+ private int mGridAreaHeight;
+ private int mCellHeight;
+ private int mScrollStartY;
+ private int mPreviousDirection;
+ private int mPreviousDistanceX;
+
+ private int mHoursTextHeight;
+ private int mEventTextAscent;
+ private int mEventTextHeight;
+ private int mAllDayHeight;
+ private int mBannerPlusMargin;
+ private int mMaxAllDayEvents;
+
+ protected int mNumDays = 7;
+ private int mNumHours = 10;
+ private int mHoursWidth;
+ private int mDateStrWidth;
+ private int mFirstCell;
+ private int mFirstHour = -1;
+ private int mFirstHourOffset;
+ private String[] mHourStrs;
+ private String[] mDayStrs;
+ private String[] mDayStrs2Letter;
+ private boolean mIs24HourFormat;
+
+ private float[] mCharWidths = new float[MAX_EVENT_TEXT_LEN];
+ private ArrayList<Event> mSelectedEvents = new ArrayList<Event>();
+ private boolean mComputeSelectedEvents;
+ private Event mSelectedEvent;
+ private Event mPrevSelectedEvent;
+ private Rect mPrevBox = new Rect();
+ protected final Resources mResources;
+ private String mAmString;
+ private String mPmString;
+ private DeleteEventHelper mDeleteEventHelper;
+
+ private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler();
+
+ /**
+ * The initial state of the touch mode when we enter this view.
+ */
+ private static final int TOUCH_MODE_INITIAL_STATE = 0;
+
+ /**
+ * Indicates we just received the touch event and we are waiting to see if
+ * it is a tap or a scroll gesture.
+ */
+ private static final int TOUCH_MODE_DOWN = 1;
+
+ /**
+ * Indicates the touch gesture is a vertical scroll
+ */
+ private static final int TOUCH_MODE_VSCROLL = 0x20;
+
+ /**
+ * Indicates the touch gesture is a horizontal scroll
+ */
+ private static final int TOUCH_MODE_HSCROLL = 0x40;
+
+ private int mTouchMode = TOUCH_MODE_INITIAL_STATE;
+
+ /**
+ * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS.
+ */
+ private static final int SELECTION_HIDDEN = 0;
+ private static final int SELECTION_PRESSED = 1;
+ private static final int SELECTION_SELECTED = 2;
+ private static final int SELECTION_LONGPRESS = 3;
+
+ private int mSelectionMode = SELECTION_HIDDEN;
+
+ private boolean mScrolling = false;
+
+ private String mDateRange;
+ private TextView mTitleTextView;
+
+ public CalendarView(CalendarActivity activity) {
+ super(activity);
+ mResources = activity.getResources();
+ mEventLoader = activity.mEventLoader;
+ mEventGeometry = new EventGeometry();
+ mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT);
+ mEventGeometry.setHourGap(HOUR_GAP);
+ mParentActivity = activity;
+ mCalendarApp = (CalendarApplication) mParentActivity.getApplication();
+ mDeleteEventHelper = new DeleteEventHelper(activity, false /* don't exit when done */);
+
+ init(activity);
+ }
+
+ private void init(Context context) {
+ setFocusable(true);
+
+ // Allow focus in touch mode so that we can do keyboard shortcuts
+ // even after we've entered touch mode.
+ setFocusableInTouchMode(true);
+ setClickable(true);
+ setOnCreateContextMenuListener(this);
+
+ mStartDay = Calendar.getInstance().getFirstDayOfWeek();
+ if (mStartDay == Calendar.SATURDAY) {
+ mStartDay = Time.SATURDAY;
+ } else if (mStartDay == Calendar.MONDAY) {
+ mStartDay = Time.MONDAY;
+ } else {
+ mStartDay = Time.SUNDAY;
+ }
+
+ mSelectionColor = mResources.getColor(R.color.selection);
+ mPressedColor = mResources.getColor(R.color.pressed);
+ mSelectedEventTextColor = mResources.getColor(R.color.calendar_event_selected_text_color);
+ mEventTextColor = mResources.getColor(R.color.calendar_event_text_color);
+ mEventTextPaint.setColor(mEventTextColor);
+ mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE);
+ mEventTextPaint.setTextAlign(Paint.Align.LEFT);
+ mEventTextPaint.setAntiAlias(true);
+
+ int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color);
+ Paint p = mSelectionPaint;
+ p.setColor(gridLineColor);
+ p.setStyle(Style.STROKE);
+ p.setStrokeWidth(2.0f);
+ p.setAntiAlias(false);
+
+ p = mPaint;
+ p.setAntiAlias(true);
+
+ // Allocate space for 2 weeks worth of weekday names so that we can
+ // easily start the week display at any week day.
+ mDayStrs = new String[14];
+
+ // Also create an array of 2-letter abbreviations.
+ mDayStrs2Letter = new String[14];
+
+ for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
+ int index = i - Calendar.SUNDAY;
+ // e.g. Tue for Tuesday
+ mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM);
+ mDayStrs[index + 7] = mDayStrs[index];
+ // e.g. Tu for Tuesday
+ mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT);
+ mDayStrs2Letter[index + 7] = mDayStrs2Letter[index];
+ }
+
+ // Figure out how much space we need for the 3-letter abbrev names
+ // in the worst case.
+ p.setTextSize(NORMAL_FONT_SIZE);
+ p.setTypeface(mBold);
+ String[] dateStrs = {" 28", " 30"};
+ mDateStrWidth = computeMaxStringWidth(0, dateStrs, p);
+ mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p);
+
+ p.setTextSize(HOURS_FONT_SIZE);
+ p.setTypeface(null);
+ mIs24HourFormat = DateFormat.is24HourFormat(context);
+ mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm;
+ mHoursWidth = computeMaxStringWidth(0, mHourStrs, p);
+
+ mAmString = DateUtils.getAMPMString(Calendar.AM);
+ mPmString = DateUtils.getAMPMString(Calendar.PM);
+ String[] ampm = {mAmString, mPmString};
+ p.setTextSize(AMPM_FONT_SIZE);
+ mHoursWidth = computeMaxStringWidth(mHoursWidth, ampm, p);
+ mHoursWidth += HOURS_MARGIN;
+
+ LayoutInflater inflater;
+ inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mPopupView = inflater.inflate(R.layout.bubble_event, null);
+ mPopupView.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ mPopup = new PopupWindow(context);
+ mPopup.setContentView(mPopupView);
+ Resources.Theme dialogTheme = getResources().newTheme();
+ dialogTheme.applyStyle(android.R.style.Theme_Dialog, true);
+ TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] {
+ android.R.attr.windowBackground });
+ mPopup.setBackgroundDrawable(ta.getDrawable(0));
+ ta.recycle();
+
+ // Enable touching the popup window
+ mPopupView.setOnClickListener(this);
+
+ mBaseDate = new Time();
+ long millis = System.currentTimeMillis();
+ mBaseDate.set(millis);
+
+ mEarliestStartHour = new int[mNumDays];
+ mHasAllDayEvent = new boolean[mNumDays];
+
+ mNumHours = context.getResources().getInteger(R.integer.number_of_hours);
+ mTitleTextView = (TextView) mParentActivity.findViewById(R.id.title);
+ }
+
+ /**
+ * This is called when the popup window is pressed.
+ */
+ public void onClick(View v) {
+ if (v == mPopupView) {
+ // Pretend it was a trackball click because that will always
+ // jump to the "View event" screen.
+ switchViews(true /* trackball */);
+ }
+ }
+
+ /**
+ * Returns the start of the selected time in milliseconds since the epoch.
+ *
+ * @return selected time in UTC milliseconds since the epoch.
+ */
+ long getSelectedTimeInMillis() {
+ Time time = new Time(mBaseDate);
+ time.setJulianDay(mSelectionDay);
+ time.hour = mSelectionHour;
+
+ // We ignore the "isDst" field because we want normalize() to figure
+ // out the correct DST value and not adjust the selected time based
+ // on the current setting of DST.
+ return time.normalize(true /* ignore isDst */);
+ }
+
+ Time getSelectedTime() {
+ Time time = new Time(mBaseDate);
+ time.setJulianDay(mSelectionDay);
+ time.hour = mSelectionHour;
+
+ // We ignore the "isDst" field because we want normalize() to figure
+ // out the correct DST value and not adjust the selected time based
+ // on the current setting of DST.
+ time.normalize(true /* ignore isDst */);
+ return time;
+ }
+
+ /**
+ * Returns the start of the selected time in minutes since midnight,
+ * local time. The derived class must ensure that this is consistent
+ * with the return value from getSelectedTimeInMillis().
+ */
+ int getSelectedMinutesSinceMidnight() {
+ return mSelectionHour * MINUTES_PER_HOUR;
+ }
+
+ public void setSelectedDay(Time time) {
+ mBaseDate.set(time);
+ mSelectionHour = mBaseDate.hour;
+ mSelectedEvent = null;
+ mPrevSelectedEvent = null;
+ long millis = mBaseDate.toMillis(false /* use isDst */);
+ mSelectionDay = Time.getJulianDay(millis, mBaseDate.gmtoff);
+ mSelectedEvents.clear();
+ mComputeSelectedEvents = true;
+
+ // Force a recalculation of the first visible hour
+ mFirstHour = -1;
+ recalc();
+ mTitleTextView.setText(mDateRange);
+
+ // Force a redraw of the selection box.
+ mSelectionMode = SELECTION_SELECTED;
+ mRedrawScreen = true;
+ mRemeasure = true;
+ invalidate();
+ }
+
+ public Time getSelectedDay() {
+ Time time = new Time(mBaseDate);
+ time.setJulianDay(mSelectionDay);
+ time.hour = mSelectionHour;
+
+ // We ignore the "isDst" field because we want normalize() to figure
+ // out the correct DST value and not adjust the selected time based
+ // on the current setting of DST.
+ time.normalize(true /* ignore isDst */);
+ return time;
+ }
+
+ private void recalc() {
+ // Set the base date to the beginning of the week if we are displaying
+ // 7 days at a time.
+ if (mNumDays == 7) {
+ int dayOfWeek = mBaseDate.weekDay;
+ int diff = dayOfWeek - mStartDay;
+ if (diff != 0) {
+ if (diff < 0) {
+ diff += 7;
+ }
+ mBaseDate.monthDay -= diff;
+ mBaseDate.normalize(true /* ignore isDst */);
+ }
+ }
+
+ final long start = mBaseDate.toMillis(false /* use isDst */);
+ long end = start;
+ mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff);
+ mLastJulianDay = mFirstJulianDay + mNumDays - 1;
+
+ mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY);
+ mFirstDate = mBaseDate.monthDay;
+
+ int flags = DateUtils.FORMAT_SHOW_YEAR;
+ if (DateFormat.is24HourFormat(mContext)) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ }
+ if (mNumDays > 1) {
+ mBaseDate.monthDay += mNumDays - 1;
+ end = mBaseDate.toMillis(true /* ignore isDst */);
+ mBaseDate.monthDay -= mNumDays - 1;
+ flags |= DateUtils.FORMAT_NO_MONTH_DAY;
+ } else {
+ flags |= DateUtils.FORMAT_SHOW_WEEKDAY
+ | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH;
+ }
+
+ mDateRange = DateUtils.formatDateRange(mParentActivity, start, end, flags);
+ // Do not set the title here because this is called when executing
+ // initNextView() to prepare the Day view when sliding the finger
+ // horizontally but we don't always want to change the title. And
+ // if we change the title here and then change it back in the caller
+ // then we get an annoying flicker.
+ }
+
+ void setDetailedView(String detailedView) {
+ mDetailedView = detailedView;
+ }
+
+ @Override
+ protected void onSizeChanged(int width, int height, int oldw, int oldh) {
+ mViewWidth = width;
+ mViewHeight = height;
+ int gridAreaWidth = width - mHoursWidth;
+ mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays;
+
+ Paint p = new Paint();
+ p.setTextSize(NORMAL_FONT_SIZE);
+ int bannerTextHeight = (int) Math.abs(p.ascent());
+
+ p.setTextSize(HOURS_FONT_SIZE);
+ mHoursTextHeight = (int) Math.abs(p.ascent());
+
+ p.setTextSize(EVENT_TEXT_FONT_SIZE);
+ float ascent = -p.ascent();
+ mEventTextAscent = (int) Math.ceil(ascent);
+ float totalHeight = ascent + p.descent();
+ mEventTextHeight = (int) Math.ceil(totalHeight);
+
+ if (mNumDays > 1) {
+ mBannerPlusMargin = bannerTextHeight + 14;
+ } else {
+ mBannerPlusMargin = 0;
+ }
+
+ remeasure(width, height);
+ }
+
+ // Measures the space needed for various parts of the view after
+ // loading new events. This can change if there are all-day events.
+ private void remeasure(int width, int height) {
+
+ // First, clear the array of earliest start times, and the array
+ // indicating presence of an all-day event.
+ for (int day = 0; day < mNumDays; day++) {
+ mEarliestStartHour[day] = 25; // some big number
+ mHasAllDayEvent[day] = false;
+ }
+
+ // Compute the space needed for the all-day events, if any.
+ // Make a pass over all the events, and keep track of the maximum
+ // number of all-day events in any one day. Also, keep track of
+ // the earliest event in each day.
+ int maxAllDayEvents = 0;
+ ArrayList<Event> events = mEvents;
+ int len = events.size();
+ for (int ii = 0; ii < len; ii++) {
+ Event event = events.get(ii);
+ if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay)
+ continue;
+ if (event.allDay) {
+ int max = event.getColumn() + 1;
+ if (maxAllDayEvents < max) {
+ maxAllDayEvents = max;
+ }
+ int daynum = event.startDay - mFirstJulianDay;
+ int durationDays = event.endDay - event.startDay + 1;
+ if (daynum < 0) {
+ durationDays += daynum;
+ daynum = 0;
+ }
+ if (daynum + durationDays > mNumDays) {
+ durationDays = mNumDays - daynum;
+ }
+ for (int day = daynum; durationDays > 0; day++, durationDays--) {
+ mHasAllDayEvent[day] = true;
+ }
+ } else {
+ int daynum = event.startDay - mFirstJulianDay;
+ int hour = event.startTime / 60;
+ if (daynum >= 0 && hour < mEarliestStartHour[daynum]) {
+ mEarliestStartHour[daynum] = hour;
+ }
+
+ // Also check the end hour in case the event spans more than
+ // one day.
+ daynum = event.endDay - mFirstJulianDay;
+ hour = event.endTime / 60;
+ if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) {
+ mEarliestStartHour[daynum] = hour;
+ }
+ }
+ }
+ mMaxAllDayEvents = maxAllDayEvents;
+
+ mFirstCell = mBannerPlusMargin;
+ int allDayHeight = 0;
+ if (maxAllDayEvents > 0) {
+ // If there is at most one all-day event per day, then use less
+ // space (but more than the space for a single event).
+ if (maxAllDayEvents == 1) {
+ allDayHeight = SINGLE_ALLDAY_HEIGHT;
+ } else {
+ // Allow the all-day area to grow in height depending on the
+ // number of all-day events we need to show, up to a limit.
+ allDayHeight = maxAllDayEvents * MAX_ALLDAY_EVENT_HEIGHT;
+ if (allDayHeight > MAX_ALLDAY_HEIGHT) {
+ allDayHeight = MAX_ALLDAY_HEIGHT;
+ }
+ }
+ mFirstCell = mBannerPlusMargin + allDayHeight + ALLDAY_TOP_MARGIN;
+ } else {
+ mSelectionAllDay = false;
+ }
+ mAllDayHeight = allDayHeight;
+
+ mGridAreaHeight = height - mFirstCell;
+ mCellHeight = (mGridAreaHeight - ((mNumHours + 1) * HOUR_GAP)) / mNumHours;
+ int usedGridAreaHeight = (mCellHeight + HOUR_GAP) * mNumHours + HOUR_GAP;
+ int bottomSpace = mGridAreaHeight - usedGridAreaHeight;
+ mEventGeometry.setHourHeight(mCellHeight);
+
+ // Create an off-screen bitmap that we can draw into.
+ mBitmapHeight = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) + bottomSpace;
+ if ((mBitmap == null || mBitmap.getHeight() < mBitmapHeight) && width > 0 &&
+ mBitmapHeight > 0) {
+ if (mBitmap != null) {
+ mBitmap.recycle();
+ }
+ mBitmap = Bitmap.createBitmap(width, mBitmapHeight, Bitmap.Config.RGB_565);
+ mCanvas = new Canvas(mBitmap);
+ }
+ mMaxViewStartY = mBitmapHeight - mGridAreaHeight;
+
+ if (mFirstHour == -1) {
+ initFirstHour();
+ mFirstHourOffset = 0;
+ }
+
+ // When we change the base date, the number of all-day events may
+ // change and that changes the cell height. When we switch dates,
+ // we use the mFirstHourOffset from the previous view, but that may
+ // be too large for the new view if the cell height is smaller.
+ if (mFirstHourOffset >= mCellHeight + HOUR_GAP) {
+ mFirstHourOffset = mCellHeight + HOUR_GAP - 1;
+ }
+ mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset;
+
+ int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP);
+ mPopup.dismiss();
+ mPopup.setWidth(eventAreaWidth - 20);
+ mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
+ }
+
+ /**
+ * Initialize the state for another view. The given view is one that has
+ * its own bitmap and will use an animation to replace the current view.
+ * The current view and new view are either both Week views or both Day
+ * views. They differ in their base date.
+ *
+ * @param view the view to initialize.
+ */
+ private void initView(CalendarView view) {
+ view.mSelectionHour = mSelectionHour;
+ view.mSelectedEvents.clear();
+ view.mComputeSelectedEvents = true;
+ view.mFirstHour = mFirstHour;
+ view.mFirstHourOffset = mFirstHourOffset;
+ view.remeasure(getWidth(), getHeight());
+
+ view.mSelectedEvent = null;
+ view.mPrevSelectedEvent = null;
+ view.mStartDay = mStartDay;
+ if (view.mEvents.size() > 0) {
+ view.mSelectionAllDay = mSelectionAllDay;
+ } else {
+ view.mSelectionAllDay = false;
+ }
+
+ // Redraw the screen so that the selection box will be redrawn. We may
+ // have scrolled to a different part of the day in some other view
+ // so the selection box in this view may no longer be visible.
+ view.mRedrawScreen = true;
+ view.recalc();
+ }
+
+ /**
+ * Switch to another view based on what was selected (an event or a free
+ * slot) and how it was selected (by touch or by trackball).
+ *
+ * @param trackBallSelection true if the selection was made using the
+ * trackball.
+ */
+ private void switchViews(boolean trackBallSelection) {
+ Event selectedEvent = mSelectedEvent;
+
+ mPopup.dismiss();
+ if (mNumDays > 1) {
+ // This is the Week view.
+ // With touch, we always switch to Day/Agenda View
+ // With track ball, if we selected a free slot, then create an event.
+ // If we selected a specific event, switch to EventInfo view.
+ if (trackBallSelection) {
+ if (selectedEvent == null) {
+ // Switch to the EditEvent view
+ long startMillis = getSelectedTimeInMillis();
+ long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setClassName(mContext, EditEvent.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, startMillis);
+ intent.putExtra(EVENT_END_TIME, endMillis);
+ mParentActivity.startActivity(intent);
+ } else {
+ // Switch to the EventInfo view
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
+ selectedEvent.id);
+ intent.setData(eventUri);
+ intent.setClassName(mContext, EventInfoActivity.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
+ intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
+ mParentActivity.startActivity(intent);
+ }
+ } else {
+ // This was a touch selection. If the touch selected a single
+ // unambiguous event, then view that event. Otherwise go to
+ // Day/Agenda view.
+ if (mSelectedEvents.size() == 1) {
+ // Switch to the EventInfo view
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
+ selectedEvent.id);
+ intent.setData(eventUri);
+ intent.setClassName(mContext, EventInfoActivity.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
+ intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
+ mParentActivity.startActivity(intent);
+ } else {
+ // Switch to the Day/Agenda view.
+ long millis = getSelectedTimeInMillis();
+ MenuHelper.switchTo(mParentActivity, mDetailedView, millis);
+ mParentActivity.finish();
+ }
+ }
+ } else {
+ // This is the Day view.
+ // If we selected a free slot, then create an event.
+ // If we selected an event, then go to the EventInfo view.
+ if (selectedEvent == null) {
+ // Switch to the EditEvent view
+ long startMillis = getSelectedTimeInMillis();
+ long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setClassName(mContext, EditEvent.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, startMillis);
+ intent.putExtra(EVENT_END_TIME, endMillis);
+ mParentActivity.startActivity(intent);
+ } else {
+ // Switch to the EventInfo view
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, selectedEvent.id);
+ intent.setData(eventUri);
+ intent.setClassName(mContext, EventInfoActivity.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
+ intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
+ mParentActivity.startActivity(intent);
+ }
+ }
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ mScrolling = false;
+ long duration = event.getEventTime() - event.getDownTime();
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (mSelectionMode == SELECTION_HIDDEN) {
+ // Don't do anything unless the selection is visible.
+ break;
+ }
+
+ if (mSelectionMode == SELECTION_PRESSED) {
+ // This was the first press when there was nothing selected.
+ // Change the selection from the "pressed" state to the
+ // the "selected" state. We treat short-press and
+ // long-press the same here because nothing was selected.
+ mSelectionMode = SELECTION_SELECTED;
+ mRedrawScreen = true;
+ invalidate();
+ break;
+ }
+
+ // Check the duration to determine if this was a short press
+ if (duration < ViewConfiguration.getLongPressTimeout()) {
+ switchViews(true /* trackball */);
+ } else {
+ mSelectionMode = SELECTION_LONGPRESS;
+ mRedrawScreen = true;
+ invalidate();
+ performLongClick();
+ }
+ break;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (mSelectionMode == SELECTION_HIDDEN) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
+ || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP
+ || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
+ // Display the selection box but don't move or select it
+ // on this key press.
+ mSelectionMode = SELECTION_SELECTED;
+ mRedrawScreen = true;
+ invalidate();
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ // Display the selection box but don't select it
+ // on this key press.
+ mSelectionMode = SELECTION_PRESSED;
+ mRedrawScreen = true;
+ invalidate();
+ return true;
+ }
+ }
+
+ mSelectionMode = SELECTION_SELECTED;
+ mScrolling = false;
+ boolean redraw;
+ int selectionDay = mSelectionDay;
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DEL:
+ // Delete the selected event, if any
+ Event selectedEvent = mSelectedEvent;
+ if (selectedEvent == null) {
+ return false;
+ }
+ mPopup.dismiss();
+
+ long begin = selectedEvent.startMillis;
+ long end = selectedEvent.endMillis;
+ long id = selectedEvent.id;
+ mDeleteEventHelper.delete(begin, end, id, -1);
+ return true;
+ case KeyEvent.KEYCODE_ENTER:
+ switchViews(true /* trackball or keyboard */);
+ return true;
+ case KeyEvent.KEYCODE_BACK:
+ mPopup.dismiss();
+ mParentActivity.finish();
+ return true;
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (mSelectedEvent != null) {
+ mSelectedEvent = mSelectedEvent.nextLeft;
+ }
+ if (mSelectedEvent == null) {
+ selectionDay -= 1;
+ }
+ redraw = true;
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (mSelectedEvent != null) {
+ mSelectedEvent = mSelectedEvent.nextRight;
+ }
+ if (mSelectedEvent == null) {
+ selectionDay += 1;
+ }
+ redraw = true;
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (mSelectedEvent != null) {
+ mSelectedEvent = mSelectedEvent.nextUp;
+ }
+ if (mSelectedEvent == null) {
+ if (!mSelectionAllDay) {
+ mSelectionHour -= 1;
+ adjustHourSelection();
+ mSelectedEvents.clear();
+ mComputeSelectedEvents = true;
+ }
+ }
+ redraw = true;
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (mSelectedEvent != null) {
+ mSelectedEvent = mSelectedEvent.nextDown;
+ }
+ if (mSelectedEvent == null) {
+ if (mSelectionAllDay) {
+ mSelectionAllDay = false;
+ } else {
+ mSelectionHour++;
+ adjustHourSelection();
+ mSelectedEvents.clear();
+ mComputeSelectedEvents = true;
+ }
+ }
+ redraw = true;
+ break;
+
+ default:
+ return super.onKeyDown(keyCode, event);
+ }
+
+ if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) {
+ boolean forward;
+ CalendarView view = mParentActivity.getNextView();
+ Time date = view.mBaseDate;
+ date.set(mBaseDate);
+ if (selectionDay < mFirstJulianDay) {
+ date.monthDay -= mNumDays;
+ forward = false;
+ } else {
+ date.monthDay += mNumDays;
+ forward = true;
+ }
+ date.normalize(true /* ignore isDst */);
+ view.mSelectionDay = selectionDay;
+
+ initView(view);
+ mTitleTextView.setText(view.mDateRange);
+ mParentActivity.switchViews(forward, 0, 0);
+ return true;
+ }
+ mSelectionDay = selectionDay;
+ mSelectedEvents.clear();
+ mComputeSelectedEvents = true;
+
+ if (redraw) {
+ mRedrawScreen = true;
+ invalidate();
+ return true;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ // This is called after scrolling stops to move the selected hour
+ // to the visible part of the screen.
+ private void resetSelectedHour() {
+ if (mSelectionHour < mFirstHour + 1) {
+ mSelectionHour = mFirstHour + 1;
+ mSelectedEvent = null;
+ mSelectedEvents.clear();
+ mComputeSelectedEvents = true;
+ } else if (mSelectionHour > mFirstHour + mNumHours - 3) {
+ mSelectionHour = mFirstHour + mNumHours - 3;
+ mSelectedEvent = null;
+ mSelectedEvents.clear();
+ mComputeSelectedEvents = true;
+ }
+ }
+
+ private void initFirstHour() {
+ mFirstHour = mSelectionHour - mNumHours / 2;
+ if (mFirstHour < 0) {
+ mFirstHour = 0;
+ } else if (mFirstHour + mNumHours > 24) {
+ mFirstHour = 24 - mNumHours;
+ }
+ }
+
+ /**
+ * Recomputes the first full hour that is visible on screen after the
+ * screen is scrolled.
+ */
+ private void computeFirstHour() {
+ // Compute the first full hour that is visible on screen
+ mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP);
+ mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY;
+ }
+
+ private void adjustHourSelection() {
+ if (mSelectionHour < 0) {
+ mSelectionHour = 0;
+ if (mMaxAllDayEvents > 0) {
+ mPrevSelectedEvent = null;
+ mSelectionAllDay = true;
+ }
+ }
+
+ if (mSelectionHour > 23) {
+ mSelectionHour = 23;
+ }
+
+ // If the selected hour is at least 2 time slots from the top and
+ // bottom of the screen, then don't scroll the view.
+ if (mSelectionHour < mFirstHour + 1) {
+ // If there are all-days events for the selected day but there
+ // are no more normal events earlier in the day, then jump to
+ // the all-day event area.
+ // Exception 1: allow the user to scroll to 8am with the trackball
+ // before jumping to the all-day event area.
+ // Exception 2: if 12am is on screen, then allow the user to select
+ // 12am before going up to the all-day event area.
+ int daynum = mSelectionDay - mFirstJulianDay;
+ if (mMaxAllDayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour
+ && mFirstHour > 0 && mFirstHour < 8) {
+ mPrevSelectedEvent = null;
+ mSelectionAllDay = true;
+ mSelectionHour = mFirstHour + 1;
+ return;
+ }
+
+ if (mFirstHour > 0) {
+ mFirstHour -= 1;
+ mViewStartY -= (mCellHeight + HOUR_GAP);
+ if (mViewStartY < 0) {
+ mViewStartY = 0;
+ }
+ return;
+ }
+ }
+
+ if (mSelectionHour > mFirstHour + mNumHours - 3) {
+ if (mFirstHour < 24 - mNumHours) {
+ mFirstHour += 1;
+ mViewStartY += (mCellHeight + HOUR_GAP);
+ if (mViewStartY > mBitmapHeight - mGridAreaHeight) {
+ mViewStartY = mBitmapHeight - mGridAreaHeight;
+ }
+ return;
+ } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) {
+ mViewStartY = mBitmapHeight - mGridAreaHeight;
+ }
+ }
+ }
+
+ void clearCachedEvents() {
+ mLastReloadMillis = 0;
+ }
+
+ private Runnable mCancelCallback = new Runnable() {
+ public void run() {
+ clearCachedEvents();
+ }
+ };
+
+ void reloadEvents() {
+ // Protect against this being called before this view has been
+ // initialized.
+ if (mParentActivity == null) {
+ return;
+ }
+
+ mSelectedEvent = null;
+ mPrevSelectedEvent = null;
+ mSelectedEvents.clear();
+
+ // The start date is the beginning of the week at 12am
+ Time weekStart = new Time();
+ weekStart.set(mBaseDate);
+ weekStart.hour = 0;
+ weekStart.minute = 0;
+ weekStart.second = 0;
+ long millis = weekStart.normalize(true /* ignore isDst */);
+
+ // Avoid reloading events unnecessarily.
+ if (millis == mLastReloadMillis) {
+ return;
+ }
+ mLastReloadMillis = millis;
+
+ // load events in the background
+ mParentActivity.startProgressSpinner();
+ final ArrayList<Event> events = new ArrayList<Event>();
+ mEventLoader.loadEventsInBackground(mNumDays, events, millis, new Runnable() {
+ public void run() {
+ mEvents = events;
+ mRemeasure = true;
+ mRedrawScreen = true;
+ mComputeSelectedEvents = true;
+ recalc();
+ mParentActivity.stopProgressSpinner();
+ invalidate();
+ }
+ }, mCancelCallback);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mRemeasure) {
+ remeasure(getWidth(), getHeight());
+ mRemeasure = false;
+ }
+
+ if (mRedrawScreen && mCanvas != null) {
+ doDraw(mCanvas);
+ mRedrawScreen = false;
+ }
+
+ if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
+ canvas.save();
+ if (mViewStartX > 0) {
+ canvas.translate(mViewWidth - mViewStartX, 0);
+ } else {
+ canvas.translate(-(mViewWidth + mViewStartX), 0);
+ }
+ CalendarView nextView = mParentActivity.getNextView();
+
+ // Prevent infinite recursive calls to onDraw().
+ nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE;
+
+ nextView.onDraw(canvas);
+ canvas.restore();
+ canvas.save();
+ canvas.translate(-mViewStartX, 0);
+ }
+
+ if (mBitmap != null) {
+ drawCalendarView(canvas);
+ }
+
+ // Draw the fixed areas (that don't scroll) directly to the canvas.
+ drawAfterScroll(canvas);
+ mComputeSelectedEvents = false;
+
+ if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
+ canvas.restore();
+ }
+ }
+
+ private void drawCalendarView(Canvas canvas) {
+
+ // Copy the scrollable region from the big bitmap to the canvas.
+ Rect src = mSrcRect;
+ Rect dest = mDestRect;
+
+ src.top = mViewStartY;
+ src.bottom = mViewStartY + mGridAreaHeight;
+ src.left = 0;
+ src.right = mViewWidth;
+
+ dest.top = mFirstCell;
+ dest.bottom = mViewHeight;
+ dest.left = 0;
+ dest.right = mViewWidth;
+ canvas.save();
+ canvas.clipRect(dest);
+ canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ canvas.drawBitmap(mBitmap, src, dest, null);
+ canvas.restore();
+ }
+
+ private void drawAfterScroll(Canvas canvas) {
+ Paint p = mPaint;
+ Rect r = mRect;
+
+ if (mMaxAllDayEvents != 0) {
+ drawAllDayEvents(mFirstJulianDay, mNumDays, r, canvas, p);
+ drawUpperLeftCorner(r, canvas, p);
+ }
+
+ if (mNumDays > 1) {
+ drawDayHeaderLoop(r, canvas, p);
+ }
+
+ // Draw the AM and PM indicators if we're in 12 hour mode
+ if (!mIs24HourFormat) {
+ drawAmPm(canvas, p);
+ }
+
+ // Update the popup window showing the event details, but only if
+ // we are not scrolling and we have focus.
+ if (!mScrolling && isFocused()) {
+ updateEventDetails();
+ }
+ }
+
+ // This isn't really the upper-left corner. It's the square area just
+ // below the upper-left corner, above the hours and to the left of the
+ // all-day area.
+ private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) {
+ p.setColor(mResources.getColor(R.color.calendar_hour_background));
+ r.top = mBannerPlusMargin;
+ r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
+ r.left = 0;
+ r.right = mHoursWidth;
+ canvas.drawRect(r, p);
+ }
+
+ private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) {
+ // Draw the horizontal day background banner
+ p.setColor(mResources.getColor(R.color.calendar_date_banner_background));
+ r.top = 0;
+ r.bottom = mBannerPlusMargin;
+ r.left = 0;
+ r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP);
+ canvas.drawRect(r, p);
+
+ // Fill the extra space on the right side with the default background
+ r.left = r.right;
+ r.right = mViewWidth;
+ p.setColor(mResources.getColor(R.color.calendar_grid_area_background));
+ canvas.drawRect(r, p);
+
+ // Draw a highlight on the selected day (if any), but only if we are
+ // displaying more than one day.
+ if (mSelectionMode != SELECTION_HIDDEN) {
+ if (mNumDays > 1) {
+ p.setColor(mResources.getColor(R.color.calendar_date_selected));
+ r.top = 0;
+ r.bottom = mBannerPlusMargin;
+ int daynum = mSelectionDay - mFirstJulianDay;
+ r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
+ r.right = r.left + mCellWidth;
+ canvas.drawRect(r, p);
+ }
+ }
+
+ p.setTextSize(NORMAL_FONT_SIZE);
+ p.setTextAlign(Paint.Align.CENTER);
+ int x = mHoursWidth;
+ int deltaX = mCellWidth + DAY_GAP;
+ int cell = mFirstJulianDay;
+
+ String[] dayNames;
+ if (mDateStrWidth < mCellWidth) {
+ dayNames = mDayStrs;
+ } else {
+ dayNames = mDayStrs2Letter;
+ }
+
+ for (int day = 0; day < mNumDays; day++, cell++) {
+ drawDayHeader(dayNames[day + mStartDay], day, cell, x, canvas, p);
+ x += deltaX;
+ }
+ }
+
+ private void drawAmPm(Canvas canvas, Paint p) {
+ p.setColor(mResources.getColor(R.color.calendar_ampm_label));
+ p.setTextSize(AMPM_FONT_SIZE);
+ p.setTypeface(mBold);
+ p.setAntiAlias(true);
+ mPaint.setTextAlign(Paint.Align.RIGHT);
+ String text = mAmString;
+ if (mFirstHour >= 12) {
+ text = mPmString;
+ }
+ int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP;
+ int right = mHoursWidth - HOURS_RIGHT_MARGIN;
+ canvas.drawText(text, right, y, p);
+
+ if (mFirstHour < 12 && mFirstHour + mNumHours > 12) {
+ // Also draw the "PM"
+ text = mPmString;
+ y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP)
+ + 2 * mHoursTextHeight + HOUR_GAP;
+ canvas.drawText(text, right, y, p);
+ }
+ }
+
+ private void doDraw(Canvas canvas) {
+ Paint p = mPaint;
+ Rect r = mRect;
+
+ drawGridBackground(r, canvas, p);
+ drawHours(r, canvas, p);
+
+ // Draw each day
+ int x = mHoursWidth;
+ int deltaX = mCellWidth + DAY_GAP;
+ int cell = mFirstJulianDay;
+ for (int day = 0; day < mNumDays; day++, cell++) {
+ drawEvents(cell, x, HOUR_GAP, canvas, p);
+ x += deltaX;
+ }
+ }
+
+ private void drawHours(Rect r, Canvas canvas, Paint p) {
+ // Draw the background for the hour labels
+ p.setColor(mResources.getColor(R.color.calendar_hour_background));
+ r.top = 0;
+ r.bottom = 24 * (mCellHeight + HOUR_GAP) + HOUR_GAP;
+ r.left = 0;
+ r.right = mHoursWidth;
+ canvas.drawRect(r, p);
+
+ // Fill the bottom left corner with the default grid background
+ r.top = r.bottom;
+ r.bottom = mBitmapHeight;
+ p.setColor(mResources.getColor(R.color.calendar_grid_area_background));
+ canvas.drawRect(r, p);
+
+ // Draw a highlight on the selected hour (if needed)
+ if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllDay) {
+ p.setColor(mResources.getColor(R.color.calendar_hour_selected));
+ r.top = mSelectionHour * (mCellHeight + HOUR_GAP);
+ r.bottom = r.top + mCellHeight + 2 * HOUR_GAP;
+ r.left = 0;
+ r.right = mHoursWidth;
+ canvas.drawRect(r, p);
+
+ // Also draw the highlight on the grid
+ p.setColor(mResources.getColor(R.color.calendar_grid_area_selected));
+ int daynum = mSelectionDay - mFirstJulianDay;
+ r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
+ r.right = r.left + mCellWidth;
+ canvas.drawRect(r, p);
+
+ // Draw a border around the highlighted grid hour.
+ Path path = mPath;
+ r.top += HOUR_GAP;
+ r.bottom -= HOUR_GAP;
+ path.reset();
+ path.addRect(r.left, r.top, r.right, r.bottom, Direction.CW);
+ canvas.drawPath(path, mSelectionPaint);
+ saveSelectionPosition(r.left, r.top, r.right, r.bottom);
+ }
+
+ p.setColor(mResources.getColor(R.color.calendar_hour_label));
+ p.setTextSize(HOURS_FONT_SIZE);
+ p.setTypeface(mBold);
+ p.setTextAlign(Paint.Align.RIGHT);
+ p.setAntiAlias(true);
+
+ int right = mHoursWidth - HOURS_RIGHT_MARGIN;
+ int y = HOUR_GAP + mHoursTextHeight;
+
+ for (int i = 0; i < 24; i++) {
+ String time = mHourStrs[i];
+ canvas.drawText(time, right, y, p);
+ y += mCellHeight + HOUR_GAP;
+ }
+ }
+
+ private void drawDayHeader(String dateStr, int day, int cell, int x, Canvas canvas, Paint p) {
+ float xCenter = x + mCellWidth / 2.0f;
+
+ p.setTypeface(mBold);
+ p.setAntiAlias(true);
+
+ boolean isWeekend = false;
+ if ((mStartDay == Time.SUNDAY && (day == 0 || day == 6))
+ || (mStartDay == Time.MONDAY && (day == 5 || day == 6))
+ || (mStartDay == Time.SATURDAY && (day == 0 || day == 1))) {
+ isWeekend = true;
+ }
+
+ if (isWeekend) {
+ p.setColor(mResources.getColor(R.color.week_weekend));
+ } else {
+ p.setColor(mResources.getColor(R.color.calendar_date_banner_text_color));
+ }
+
+ int dateNum = mFirstDate + day;
+ if (dateNum > mMonthLength) {
+ dateNum -= mMonthLength;
+ }
+
+ // Add a leading zero if the date is a single digit
+ if (dateNum < 10) {
+ dateStr += " 0" + dateNum;
+ } else {
+ dateStr += " " + dateNum;
+ }
+
+ float y = mBannerPlusMargin - 7;
+ canvas.drawText(dateStr, xCenter, y, p);
+ }
+
+ private void drawGridBackground(Rect r, Canvas canvas, Paint p) {
+ Paint.Style savedStyle = p.getStyle();
+
+ // Clear the background
+ p.setColor(mResources.getColor(R.color.calendar_grid_area_background));
+ r.top = 0;
+ r.bottom = mBitmapHeight;
+ r.left = 0;
+ r.right = mViewWidth;
+ canvas.drawRect(r, p);
+
+ // Draw the horizontal grid lines
+ p.setColor(mResources.getColor(R.color.calendar_grid_line_horizontal_color));
+ p.setStyle(Style.STROKE);
+ p.setStrokeWidth(0);
+ p.setAntiAlias(false);
+ float startX = mHoursWidth;
+ float stopX = mHoursWidth + (mCellWidth + DAY_GAP) * mNumDays;
+ float y = 0;
+ float deltaY = mCellHeight + HOUR_GAP;
+ for (int hour = 0; hour <= 24; hour++) {
+ canvas.drawLine(startX, y, stopX, y, p);
+ y += deltaY;
+ }
+
+ // Draw the vertical grid lines
+ p.setColor(mResources.getColor(R.color.calendar_grid_line_vertical_color));
+ float startY = 0;
+ float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP);
+ float deltaX = mCellWidth + DAY_GAP;
+ float x = mHoursWidth + mCellWidth;
+ for (int day = 0; day < mNumDays; day++) {
+ canvas.drawLine(x, startY, x, stopY, p);
+ x += deltaX;
+ }
+
+ // Restore the saved style.
+ p.setStyle(savedStyle);
+ p.setAntiAlias(true);
+ }
+
+ Event getSelectedEvent() {
+ if (mSelectedEvent == null) {
+ // There is no event at the selected hour, so create a new event.
+ return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
+ getSelectedMinutesSinceMidnight());
+ }
+ return mSelectedEvent;
+ }
+
+ boolean isEventSelected() {
+ return (mSelectedEvent != null);
+ }
+
+ Event getNewEvent() {
+ return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
+ getSelectedMinutesSinceMidnight());
+ }
+
+ static Event getNewEvent(int julianDay, long utcMillis,
+ int minutesSinceMidnight) {
+ Event event = Event.newInstance();
+ event.startDay = julianDay;
+ event.endDay = julianDay;
+ event.startMillis = utcMillis;
+ event.endMillis = event.startMillis + MILLIS_PER_HOUR;
+ event.startTime = minutesSinceMidnight;
+ event.endTime = event.startTime + MINUTES_PER_HOUR;
+ return event;
+ }
+
+ private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) {
+ float maxWidthF = 0.0f;
+
+ int len = strings.length;
+ for (int i = 0; i < len; i++) {
+ float width = p.measureText(strings[i]);
+ maxWidthF = Math.max(width, maxWidthF);
+ }
+ int maxWidth = (int) (maxWidthF + 0.5);
+ if (maxWidth < currentMax) {
+ maxWidth = currentMax;
+ }
+ return maxWidth;
+ }
+
+ private void saveSelectionPosition(float left, float top, float right, float bottom) {
+ mPrevBox.left = (int) left;
+ mPrevBox.right = (int) right;
+ mPrevBox.top = (int) top;
+ mPrevBox.bottom = (int) bottom;
+ }
+
+ private Rect getCurrentSelectionPosition() {
+ Rect box = new Rect();
+ box.top = mSelectionHour * (mCellHeight + HOUR_GAP);
+ box.bottom = box.top + mCellHeight + HOUR_GAP;
+ int daynum = mSelectionDay - mFirstJulianDay;
+ box.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
+ box.right = box.left + mCellWidth + DAY_GAP;
+ return box;
+ }
+
+ private void drawAllDayEvents(int firstDay, int numDays,
+ Rect r, Canvas canvas, Paint p) {
+ p.setTextSize(NORMAL_FONT_SIZE);
+ p.setTextAlign(Paint.Align.LEFT);
+ Paint eventTextPaint = mEventTextPaint;
+
+ // Draw the background for the all-day events area
+ r.top = mBannerPlusMargin;
+ r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
+ r.left = mHoursWidth;
+ r.right = r.left + mNumDays * (mCellWidth + DAY_GAP);
+ p.setColor(mResources.getColor(R.color.calendar_all_day_background));
+ canvas.drawRect(r, p);
+
+ // Fill the extra space on the right side with the default background
+ r.left = r.right;
+ r.right = mViewWidth;
+ p.setColor(mResources.getColor(R.color.calendar_grid_area_background));
+ canvas.drawRect(r, p);
+
+ // Draw the vertical grid lines
+ p.setColor(mResources.getColor(R.color.calendar_grid_line_vertical_color));
+ p.setStyle(Style.STROKE);
+ p.setStrokeWidth(0);
+ p.setAntiAlias(false);
+ float startY = r.top;
+ float stopY = r.bottom;
+ float deltaX = mCellWidth + DAY_GAP;
+ float x = mHoursWidth + mCellWidth;
+ for (int day = 0; day <= mNumDays; day++) {
+ canvas.drawLine(x, startY, x, stopY, p);
+ x += deltaX;
+ }
+ p.setAntiAlias(true);
+ p.setStyle(Style.FILL);
+
+ int y = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
+ float left = mHoursWidth;
+ int lastDay = firstDay + numDays - 1;
+ ArrayList<Event> events = mEvents;
+ int numEvents = events.size();
+ float drawHeight = mAllDayHeight;
+ float numRectangles = mMaxAllDayEvents;
+ for (int i = 0; i < numEvents; i++) {
+ Event event = events.get(i);
+ if (!event.allDay)
+ continue;
+ int startDay = event.startDay;
+ int endDay = event.endDay;
+ if (startDay > lastDay || endDay < firstDay)
+ continue;
+ if (startDay < firstDay)
+ startDay = firstDay;
+ if (endDay > lastDay)
+ endDay = lastDay;
+ int startIndex = startDay - firstDay;
+ int endIndex = endDay - firstDay;
+ float height = drawHeight / numRectangles;
+
+ // Prevent a single event from getting too big
+ if (height > MAX_ALLDAY_EVENT_HEIGHT) {
+ height = MAX_ALLDAY_EVENT_HEIGHT;
+ }
+
+ // Leave a one-pixel space between the vertical day lines and the
+ // event rectangle.
+ event.left = left + startIndex * (mCellWidth + DAY_GAP) + 2;
+ event.right = left + endIndex * (mCellWidth + DAY_GAP) + mCellWidth - 1;
+ event.top = y + height * event.getColumn();
+
+ // Multiply the height by 0.9 to leave a little gap between events
+ event.bottom = event.top + height * 0.9f;
+
+ RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint);
+ drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN);
+
+ // Check if this all-day event intersects the selected day
+ if (mSelectionAllDay && mComputeSelectedEvents) {
+ if (startDay <= mSelectionDay && endDay >= mSelectionDay) {
+ mSelectedEvents.add(event);
+ }
+ }
+ }
+
+ if (mSelectionAllDay) {
+ // Compute the neighbors for the list of all-day events that
+ // intersect the selected day.
+ computeAllDayNeighbors();
+ if (mSelectedEvent != null) {
+ Event event = mSelectedEvent;
+ RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint);
+ drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN);
+ }
+
+ // Draw the highlight on the selected all-day area
+ float top = mBannerPlusMargin + 1;
+ float bottom = top + mAllDayHeight + ALLDAY_TOP_MARGIN - 1;
+ int daynum = mSelectionDay - mFirstJulianDay;
+ left = mHoursWidth + daynum * (mCellWidth + DAY_GAP) + 1;
+ float right = left + mCellWidth + DAY_GAP - 1;
+ if (mNumDays == 1) {
+ // The Day view doesn't have a vertical line on the right.
+ right -= 1;
+ }
+ Path path = mPath;
+ path.reset();
+ path.addRect(left, top, right, bottom, Direction.CW);
+ canvas.drawPath(path, mSelectionPaint);
+
+ // Set the selection position to zero so that when we move down
+ // to the normal event area, we will highlight the topmost event.
+ saveSelectionPosition(0f, 0f, 0f, 0f);
+ }
+ }
+
+ private void computeAllDayNeighbors() {
+ int len = mSelectedEvents.size();
+ if (len == 0 || mSelectedEvent != null) {
+ return;
+ }
+
+ // First, clear all the links
+ for (int ii = 0; ii < len; ii++) {
+ Event ev = mSelectedEvents.get(ii);
+ ev.nextUp = null;
+ ev.nextDown = null;
+ ev.nextLeft = null;
+ ev.nextRight = null;
+ }
+
+ // For each event in the selected event list "mSelectedEvents", find
+ // its neighbors in the up and down directions. This could be done
+ // more efficiently by sorting on the Event.getColumn() field, but
+ // the list is expected to be very small.
+
+ // Find the event in the same row as the previously selected all-day
+ // event, if any.
+ int startPosition = -1;
+ if (mPrevSelectedEvent != null && mPrevSelectedEvent.allDay) {
+ startPosition = mPrevSelectedEvent.getColumn();
+ }
+ int maxPosition = -1;
+ Event startEvent = null;
+ Event maxPositionEvent = null;
+ for (int ii = 0; ii < len; ii++) {
+ Event ev = mSelectedEvents.get(ii);
+ int position = ev.getColumn();
+ if (position == startPosition) {
+ startEvent = ev;
+ } else if (position > maxPosition) {
+ maxPositionEvent = ev;
+ maxPosition = position;
+ }
+ for (int jj = 0; jj < len; jj++) {
+ if (jj == ii) {
+ continue;
+ }
+ Event neighbor = mSelectedEvents.get(jj);
+ int neighborPosition = neighbor.getColumn();
+ if (neighborPosition == position - 1) {
+ ev.nextUp = neighbor;
+ } else if (neighborPosition == position + 1) {
+ ev.nextDown = neighbor;
+ }
+ }
+ }
+ if (startEvent != null) {
+ mSelectedEvent = startEvent;
+ } else {
+ mSelectedEvent = maxPositionEvent;
+ }
+ }
+
+ RectF drawAllDayEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
+ // If this event is selected, then use the selection color
+ if (mSelectedEvent == event) {
+ // Also, remember the last selected event that we drew
+ mPrevSelectedEvent = event;
+ p.setColor(mSelectionColor);
+ eventTextPaint.setColor(mSelectedEventTextColor);
+ } else {
+ // Use the normal color for all-day events
+ p.setColor(event.color);
+ eventTextPaint.setColor(mEventTextColor);
+ }
+
+ RectF rf = mRectF;
+ rf.top = event.top;
+ rf.bottom = event.bottom;
+ rf.left = event.left;
+ rf.right = event.right;
+ canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
+
+ rf.left += 2;
+ rf.right -= 2;
+ return rf;
+ }
+
+ private void drawEvents(int date, int left, int top, Canvas canvas, Paint p) {
+ Paint eventTextPaint = mEventTextPaint;
+ int cellWidth = mCellWidth;
+ int cellHeight = mCellHeight;
+
+ // Use the selected hour as the selection region
+ Rect selectionArea = mRect;
+ selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP);
+ selectionArea.bottom = selectionArea.top + cellHeight;
+ selectionArea.left = left;
+ selectionArea.right = selectionArea.left + cellWidth;
+
+ ArrayList<Event> events = mEvents;
+ int numEvents = events.size();
+ EventGeometry geometry = mEventGeometry;
+
+ for (int i = 0; i < numEvents; i++) {
+ Event event = events.get(i);
+ if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
+ continue;
+ }
+
+ if (date == mSelectionDay && !mSelectionAllDay && mComputeSelectedEvents
+ && geometry.eventIntersectsSelection(event, selectionArea)) {
+ mSelectedEvents.add(event);
+ }
+
+ RectF rf = drawEventRect(event, canvas, p, eventTextPaint);
+ drawEventText(event, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
+ }
+
+ if (date == mSelectionDay && !mSelectionAllDay && isFocused()
+ && mSelectionMode != SELECTION_HIDDEN) {
+ computeNeighbors();
+ if (mSelectedEvent != null) {
+ RectF rf = drawEventRect(mSelectedEvent, canvas, p, eventTextPaint);
+ drawEventText(mSelectedEvent, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
+ }
+ }
+ }
+
+ // Computes the "nearest" neighbor event in four directions (left, right,
+ // up, down) for each of the events in the mSelectedEvents array.
+ private void computeNeighbors() {
+ int len = mSelectedEvents.size();
+ if (len == 0 || mSelectedEvent != null) {
+ return;
+ }
+
+ // First, clear all the links
+ for (int ii = 0; ii < len; ii++) {
+ Event ev = mSelectedEvents.get(ii);
+ ev.nextUp = null;
+ ev.nextDown = null;
+ ev.nextLeft = null;
+ ev.nextRight = null;
+ }
+
+ Event startEvent = mSelectedEvents.get(0);
+ int startEventDistance1 = 100000; // any large number
+ int startEventDistance2 = 100000; // any large number
+ int prevLocation = FROM_NONE;
+ int prevTop;
+ int prevBottom;
+ int prevLeft;
+ int prevRight;
+ int prevCenter = 0;
+ Rect box = getCurrentSelectionPosition();
+ if (mPrevSelectedEvent != null) {
+ prevTop = (int) mPrevSelectedEvent.top;
+ prevBottom = (int) mPrevSelectedEvent.bottom;
+ prevLeft = (int) mPrevSelectedEvent.left;
+ prevRight = (int) mPrevSelectedEvent.right;
+ // Check if the previously selected event intersects the previous
+ // selection box. (The previously selected event may be from a
+ // much older selection box.)
+ if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top
+ || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) {
+ mPrevSelectedEvent = null;
+ prevTop = mPrevBox.top;
+ prevBottom = mPrevBox.bottom;
+ prevLeft = mPrevBox.left;
+ prevRight = mPrevBox.right;
+ } else {
+ // Clip the top and bottom to the previous selection box.
+ if (prevTop < mPrevBox.top) {
+ prevTop = mPrevBox.top;
+ }
+ if (prevBottom > mPrevBox.bottom) {
+ prevBottom = mPrevBox.bottom;
+ }
+ }
+ } else {
+ // Just use the previously drawn selection box
+ prevTop = mPrevBox.top;
+ prevBottom = mPrevBox.bottom;
+ prevLeft = mPrevBox.left;
+ prevRight = mPrevBox.right;
+ }
+
+ // Figure out where we came from and compute the center of that area.
+ if (prevLeft >= box.right) {
+ // The previously selected event was to the right of us.
+ prevLocation = FROM_RIGHT;
+ prevCenter = (prevTop + prevBottom) / 2;
+ } else if (prevRight <= box.left) {
+ // The previously selected event was to the left of us.
+ prevLocation = FROM_LEFT;
+ prevCenter = (prevTop + prevBottom) / 2;
+ } else if (prevBottom <= box.top) {
+ // The previously selected event was above us.
+ prevLocation = FROM_ABOVE;
+ prevCenter = (prevLeft + prevRight) / 2;
+ } else if (prevTop >= box.bottom) {
+ // The previously selected event was below us.
+ prevLocation = FROM_BELOW;
+ prevCenter = (prevLeft + prevRight) / 2;
+ }
+
+ // For each event in the selected event list "mSelectedEvents", search
+ // all the other events in that list for the nearest neighbor in 4
+ // directions.
+ for (int ii = 0; ii < len; ii++) {
+ Event ev = mSelectedEvents.get(ii);
+
+ int startTime = ev.startTime;
+ int endTime = ev.endTime;
+ int left = (int) ev.left;
+ int right = (int) ev.right;
+ int top = (int) ev.top;
+ if (top < box.top) {
+ top = box.top;
+ }
+ int bottom = (int) ev.bottom;
+ if (bottom > box.bottom) {
+ bottom = box.bottom;
+ }
+ if (false) {
+ int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
+ | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
+ if (DateFormat.is24HourFormat(mContext)) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ }
+ String timeRange = DateUtils.formatDateRange(mParentActivity,
+ ev.startMillis, ev.endMillis, flags);
+ Log.i("Cal", "left: " + left + " right: " + right + " top: " + top
+ + " bottom: " + bottom + " ev: " + timeRange + " " + ev.title);
+ }
+ int upDistanceMin = 10000; // any large number
+ int downDistanceMin = 10000; // any large number
+ int leftDistanceMin = 10000; // any large number
+ int rightDistanceMin = 10000; // any large number
+ Event upEvent = null;
+ Event downEvent = null;
+ Event leftEvent = null;
+ Event rightEvent = null;
+
+ // Pick the starting event closest to the previously selected event,
+ // if any. distance1 takes precedence over distance2.
+ int distance1 = 0;
+ int distance2 = 0;
+ if (prevLocation == FROM_ABOVE) {
+ if (left >= prevCenter) {
+ distance1 = left - prevCenter;
+ } else if (right <= prevCenter) {
+ distance1 = prevCenter - right;
+ }
+ distance2 = top - prevBottom;
+ } else if (prevLocation == FROM_BELOW) {
+ if (left >= prevCenter) {
+ distance1 = left - prevCenter;
+ } else if (right <= prevCenter) {
+ distance1 = prevCenter - right;
+ }
+ distance2 = prevTop - bottom;
+ } else if (prevLocation == FROM_LEFT) {
+ if (bottom <= prevCenter) {
+ distance1 = prevCenter - bottom;
+ } else if (top >= prevCenter) {
+ distance1 = top - prevCenter;
+ }
+ distance2 = left - prevRight;
+ } else if (prevLocation == FROM_RIGHT) {
+ if (bottom <= prevCenter) {
+ distance1 = prevCenter - bottom;
+ } else if (top >= prevCenter) {
+ distance1 = top - prevCenter;
+ }
+ distance2 = prevLeft - right;
+ }
+ if (distance1 < startEventDistance1
+ || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) {
+ startEvent = ev;
+ startEventDistance1 = distance1;
+ startEventDistance2 = distance2;
+ }
+
+ // For each neighbor, figure out if it is above or below or left
+ // or right of me and compute the distance.
+ for (int jj = 0; jj < len; jj++) {
+ if (jj == ii) {
+ continue;
+ }
+ Event neighbor = mSelectedEvents.get(jj);
+ int neighborLeft = (int) neighbor.left;
+ int neighborRight = (int) neighbor.right;
+ if (neighbor.endTime <= startTime) {
+ // This neighbor is entirely above me.
+ // If we overlap the same column, then compute the distance.
+ if (neighborLeft < right && neighborRight > left) {
+ int distance = startTime - neighbor.endTime;
+ if (distance < upDistanceMin) {
+ upDistanceMin = distance;
+ upEvent = neighbor;
+ } else if (distance == upDistanceMin) {
+ int center = (left + right) / 2;
+ int currentDistance = 0;
+ int currentLeft = (int) upEvent.left;
+ int currentRight = (int) upEvent.right;
+ if (currentRight <= center) {
+ currentDistance = center - currentRight;
+ } else if (currentLeft >= center) {
+ currentDistance = currentLeft - center;
+ }
+
+ int neighborDistance = 0;
+ if (neighborRight <= center) {
+ neighborDistance = center - neighborRight;
+ } else if (neighborLeft >= center) {
+ neighborDistance = neighborLeft - center;
+ }
+ if (neighborDistance < currentDistance) {
+ upDistanceMin = distance;
+ upEvent = neighbor;
+ }
+ }
+ }
+ } else if (neighbor.startTime >= endTime) {
+ // This neighbor is entirely below me.
+ // If we overlap the same column, then compute the distance.
+ if (neighborLeft < right && neighborRight > left) {
+ int distance = neighbor.startTime - endTime;
+ if (distance < downDistanceMin) {
+ downDistanceMin = distance;
+ downEvent = neighbor;
+ } else if (distance == downDistanceMin) {
+ int center = (left + right) / 2;
+ int currentDistance = 0;
+ int currentLeft = (int) downEvent.left;
+ int currentRight = (int) downEvent.right;
+ if (currentRight <= center) {
+ currentDistance = center - currentRight;
+ } else if (currentLeft >= center) {
+ currentDistance = currentLeft - center;
+ }
+
+ int neighborDistance = 0;
+ if (neighborRight <= center) {
+ neighborDistance = center - neighborRight;
+ } else if (neighborLeft >= center) {
+ neighborDistance = neighborLeft - center;
+ }
+ if (neighborDistance < currentDistance) {
+ downDistanceMin = distance;
+ downEvent = neighbor;
+ }
+ }
+ }
+ }
+
+ if (neighborLeft >= right) {
+ // This neighbor is entirely to the right of me.
+ // Take the closest neighbor in the y direction.
+ int center = (top + bottom) / 2;
+ int distance = 0;
+ int neighborBottom = (int) neighbor.bottom;
+ int neighborTop = (int) neighbor.top;
+ if (neighborBottom <= center) {
+ distance = center - neighborBottom;
+ } else if (neighborTop >= center) {
+ distance = neighborTop - center;
+ }
+ if (distance < rightDistanceMin) {
+ rightDistanceMin = distance;
+ rightEvent = neighbor;
+ } else if (distance == rightDistanceMin) {
+ // Pick the closest in the x direction
+ int neighborDistance = neighborLeft - right;
+ int currentDistance = (int) rightEvent.left - right;
+ if (neighborDistance < currentDistance) {
+ rightDistanceMin = distance;
+ rightEvent = neighbor;
+ }
+ }
+ } else if (neighborRight <= left) {
+ // This neighbor is entirely to the left of me.
+ // Take the closest neighbor in the y direction.
+ int center = (top + bottom) / 2;
+ int distance = 0;
+ int neighborBottom = (int) neighbor.bottom;
+ int neighborTop = (int) neighbor.top;
+ if (neighborBottom <= center) {
+ distance = center - neighborBottom;
+ } else if (neighborTop >= center) {
+ distance = neighborTop - center;
+ }
+ if (distance < leftDistanceMin) {
+ leftDistanceMin = distance;
+ leftEvent = neighbor;
+ } else if (distance == leftDistanceMin) {
+ // Pick the closest in the x direction
+ int neighborDistance = left - neighborRight;
+ int currentDistance = left - (int) leftEvent.right;
+ if (neighborDistance < currentDistance) {
+ leftDistanceMin = distance;
+ leftEvent = neighbor;
+ }
+ }
+ }
+ }
+ ev.nextUp = upEvent;
+ ev.nextDown = downEvent;
+ ev.nextLeft = leftEvent;
+ ev.nextRight = rightEvent;
+ }
+ mSelectedEvent = startEvent;
+ }
+
+
+ private RectF drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
+
+ int color = event.color;
+
+ // Fade visible boxes if event was declined.
+ boolean declined = (event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED);
+ if (declined) {
+ int alpha = color & 0xff000000;
+ color &= 0x00ffffff;
+ int red = (color & 0x00ff0000) >> 16;
+ int green = (color & 0x0000ff00) >> 8;
+ int blue = (color & 0x0000ff);
+ color = ((red >> 1) << 16) | ((green >> 1) << 8) | (blue >> 1);
+ color += 0x7F7F7F + alpha;
+ }
+
+ // If this event is selected, then use the selection color
+ if (mSelectedEvent == event) {
+ if (mSelectionMode == SELECTION_PRESSED) {
+ // Also, remember the last selected event that we drew
+ mPrevSelectedEvent = event;
+ // box = mBoxPressed;
+ p.setColor(mPressedColor); // FIXME:pressed
+ eventTextPaint.setColor(mSelectedEventTextColor);
+ } else if (mSelectionMode == SELECTION_SELECTED) {
+ // Also, remember the last selected event that we drew
+ mPrevSelectedEvent = event;
+ // box = mBoxSelected;
+ p.setColor(mSelectionColor);
+ eventTextPaint.setColor(mSelectedEventTextColor);
+ } else if (mSelectionMode == SELECTION_LONGPRESS) {
+ // box = mBoxLongPressed;
+ p.setColor(mPressedColor); // FIXME: longpressed (maybe -- this doesn't seem to work)
+ eventTextPaint.setColor(mSelectedEventTextColor);
+ } else {
+ p.setColor(color);
+ eventTextPaint.setColor(mEventTextColor);
+ }
+ } else {
+ p.setColor(color);
+ eventTextPaint.setColor(mEventTextColor);
+ }
+
+
+ RectF rf = mRectF;
+ rf.top = event.top;
+ rf.bottom = event.bottom;
+ rf.left = event.left;
+ rf.right = event.right - 1;
+
+ canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
+
+ rf.left += 2;
+ rf.right -= 2;
+
+ return rf;
+ }
+
+ private void drawEventText(Event event, RectF rf, Canvas canvas, Paint p, int topMargin) {
+ if (!mDrawTextInEventRect) {
+ return;
+ }
+
+ float width = rf.right - rf.left;
+ float height = rf.bottom - rf.top;
+
+ // Leave one pixel extra space between lines
+ int lineHeight = mEventTextHeight + 1;
+
+ // If the rectangle is too small for text, then return
+ if (width < MIN_CELL_WIDTH_FOR_TEXT || height <= lineHeight) {
+ return;
+ }
+
+ // Truncate the event title to a known (large enough) limit
+ String text = event.getTitleAndLocation();
+ int len = text.length();
+ if (len > MAX_EVENT_TEXT_LEN) {
+ text = text.substring(0, MAX_EVENT_TEXT_LEN);
+ len = MAX_EVENT_TEXT_LEN;
+ }
+
+ // Figure out how much space the event title will take, and create a
+ // String fragment that will fit in the rectangle. Use multiple lines,
+ // if available.
+ p.getTextWidths(text, mCharWidths);
+ String fragment = text;
+ float top = rf.top + mEventTextAscent + topMargin;
+ int start = 0;
+
+ // Leave one pixel extra space at the bottom
+ while (start < len && height >= (lineHeight + 1)) {
+ boolean lastLine = (height < 2 * lineHeight + 1);
+ // Skip leading spaces at the beginning of each line
+ do {
+ char c = text.charAt(start);
+ if (c != ' ') break;
+ start += 1;
+ } while (start < len);
+
+ float sum = 0;
+ int end = start;
+ for (int ii = start; ii < len; ii++) {
+ char c = text.charAt(ii);
+
+ // If we found the end of a word, then remember the ending
+ // position.
+ if (c == ' ') {
+ end = ii;
+ }
+ sum += mCharWidths[ii];
+ // If adding this character would exceed the width and this
+ // isn't the last line, then break the line at the previous
+ // word. If there was no previous word, then break this word.
+ if (sum > width) {
+ if (end > start && !lastLine) {
+ // There was a previous word on this line.
+ fragment = text.substring(start, end);
+ start = end;
+ break;
+ }
+
+ // This is the only word and it is too long to fit on
+ // the line (or this is the last line), so take as many
+ // characters of this word as will fit.
+ fragment = text.substring(start, ii);
+ start = ii;
+ break;
+ }
+ }
+
+ // If sum <= width, then we can fit the rest of the text on
+ // this line.
+ if (sum <= width) {
+ fragment = text.substring(start, len);
+ start = len;
+ }
+
+ canvas.drawText(fragment, rf.left + 1, top, p);
+
+ top += lineHeight;
+ height -= lineHeight;
+ }
+ }
+
+ private void updateEventDetails() {
+ if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN
+ || mSelectionMode == SELECTION_LONGPRESS) {
+ mPopup.dismiss();
+ return;
+ }
+
+ // Remove any outstanding callbacks to dismiss the popup.
+ getHandler().removeCallbacks(mDismissPopup);
+
+ Event event = mSelectedEvent;
+ TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title);
+ titleView.setText(event.title);
+
+ ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon);
+ imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE);
+
+ imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon);
+ imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE);
+
+ int flags;
+ if (event.allDay) {
+ flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE |
+ DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL;
+ } else {
+ flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL
+ | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
+ }
+ if (DateFormat.is24HourFormat(mContext)) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ }
+ String timeRange = DateUtils.formatDateRange(mParentActivity,
+ event.startMillis, event.endMillis, flags);
+ TextView timeView = (TextView) mPopupView.findViewById(R.id.time);
+ timeView.setText(timeRange);
+
+ TextView whereView = (TextView) mPopupView.findViewById(R.id.where);
+ final boolean empty = TextUtils.isEmpty(event.location);
+ whereView.setVisibility(empty ? View.GONE : View.VISIBLE);
+ if (!empty) whereView.setText(event.location);
+
+ mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5);
+ postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);
+ }
+
+ // The following routines are called from the parent activity when certain
+ // touch events occur.
+
+ void doDown(MotionEvent ev) {
+ mTouchMode = TOUCH_MODE_DOWN;
+ mViewStartX = 0;
+ mOnFlingCalled = false;
+ mLaunchNewView = false;
+ getHandler().removeCallbacks(mContinueScroll);
+ }
+
+ void doSingleTapUp(MotionEvent ev) {
+ mSelectionMode = SELECTION_SELECTED;
+ mRedrawScreen = true;
+ invalidate();
+ if (mLaunchNewView) {
+ mLaunchNewView = false;
+ switchViews(false /* not the trackball */);
+ }
+ }
+
+ void doShowPress(MotionEvent ev) {
+ int x = (int) ev.getX();
+ int y = (int) ev.getY();
+ Event selectedEvent = mSelectedEvent;
+ int selectedDay = mSelectionDay;
+ int selectedHour = mSelectionHour;
+
+ boolean validPosition = setSelectionFromPosition(x, y);
+ if (!validPosition) {
+ return;
+ }
+
+ mSelectionMode = SELECTION_PRESSED;
+ mRedrawScreen = true;
+ invalidate();
+
+ // If the tap is on an already selected event or hour slot,
+ // then launch a new view. Otherwise, just select the event.
+ if (selectedEvent != null && selectedEvent == mSelectedEvent) {
+ // Launch the "View event" view when the finger lifts up,
+ // unless the finger moves before lifting up.
+ mLaunchNewView = true;
+ } else if (selectedEvent == null && selectedDay == mSelectionDay
+ && selectedHour == mSelectionHour) {
+ // Launch the Day/Agenda view when the finger lifts up,
+ // unless the finger moves before lifting up.
+ mLaunchNewView = true;
+ }
+ }
+
+ void doLongPress(MotionEvent ev) {
+ mLaunchNewView = false;
+ mSelectionMode = SELECTION_LONGPRESS;
+ mRedrawScreen = true;
+ invalidate();
+ performLongClick();
+ }
+
+ void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) {
+ mLaunchNewView = false;
+ // Use the distance from the current point to the initial touch instead
+ // of deltaX and deltaY to avoid accumulating floating-point rounding
+ // errors. Also, we don't need floats, we can use ints.
+ int distanceX = (int) e1.getX() - (int) e2.getX();
+ int distanceY = (int) e1.getY() - (int) e2.getY();
+
+ // If we haven't figured out the predominant scroll direction yet,
+ // then do it now.
+ if (mTouchMode == TOUCH_MODE_DOWN) {
+ int absDistanceX = Math.abs(distanceX);
+ int absDistanceY = Math.abs(distanceY);
+ mScrollStartY = mViewStartY;
+ mPreviousDistanceX = 0;
+ mPreviousDirection = 0;
+
+ // If the x distance is at least twice the y distance, then lock
+ // the scroll horizontally. Otherwise scroll vertically.
+ if (absDistanceX >= 2 * absDistanceY) {
+ mTouchMode = TOUCH_MODE_HSCROLL;
+ mViewStartX = distanceX;
+ initNextView(-mViewStartX);
+ } else {
+ mTouchMode = TOUCH_MODE_VSCROLL;
+ }
+ } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
+ // We are already scrolling horizontally, so check if we
+ // changed the direction of scrolling so that the other week
+ // is now visible.
+ mViewStartX = distanceX;
+ if (distanceX != 0) {
+ int direction = (distanceX > 0) ? 1 : -1;
+ if (direction != mPreviousDirection) {
+ // The user has switched the direction of scrolling
+ // so re-init the next view
+ initNextView(-mViewStartX);
+ mPreviousDirection = direction;
+ }
+ }
+
+ // If we have moved at least the HORIZONTAL_SCROLL_THRESHOLD,
+ // then change the title to the new day (or week), but only
+ // if we haven't already changed the title.
+ if (distanceX >= HORIZONTAL_SCROLL_THRESHOLD) {
+ if (mPreviousDistanceX < HORIZONTAL_SCROLL_THRESHOLD) {
+ CalendarView view = mParentActivity.getNextView();
+ mTitleTextView.setText(view.mDateRange);
+ }
+ } else if (distanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
+ if (mPreviousDistanceX > -HORIZONTAL_SCROLL_THRESHOLD) {
+ CalendarView view = mParentActivity.getNextView();
+ mTitleTextView.setText(view.mDateRange);
+ }
+ } else {
+ if (mPreviousDistanceX >= HORIZONTAL_SCROLL_THRESHOLD
+ || mPreviousDistanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
+ mTitleTextView.setText(mDateRange);
+ }
+ }
+ mPreviousDistanceX = distanceX;
+ }
+
+ if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) {
+ mViewStartY = mScrollStartY + distanceY;
+ if (mViewStartY < 0) {
+ mViewStartY = 0;
+ } else if (mViewStartY > mMaxViewStartY) {
+ mViewStartY = mMaxViewStartY;
+ }
+ computeFirstHour();
+ }
+
+ mScrolling = true;
+
+ if (mSelectionMode != SELECTION_HIDDEN) {
+ mSelectionMode = SELECTION_HIDDEN;
+ mRedrawScreen = true;
+ }
+ invalidate();
+ }
+
+ void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ mTouchMode = TOUCH_MODE_INITIAL_STATE;
+ mSelectionMode = SELECTION_HIDDEN;
+ mOnFlingCalled = true;
+ int deltaX = (int) e2.getX() - (int) e1.getX();
+ int distanceX = Math.abs(deltaX);
+ int deltaY = (int) e2.getY() - (int) e1.getY();
+ int distanceY = Math.abs(deltaY);
+
+ if ((distanceX >= HORIZONTAL_SCROLL_THRESHOLD) && (distanceX > distanceY)) {
+ boolean switchForward = initNextView(deltaX);
+ CalendarView view = mParentActivity.getNextView();
+ mTitleTextView.setText(view.mDateRange);
+ mParentActivity.switchViews(switchForward, mViewStartX, mViewWidth);
+ mViewStartX = 0;
+ return;
+ }
+
+ // Continue scrolling vertically
+ mContinueScroll.init((int) velocityY / 20);
+ post(mContinueScroll);
+ }
+
+ private boolean initNextView(int deltaX) {
+ // Change the view to the previous day or week
+ CalendarView view = mParentActivity.getNextView();
+ Time date = view.mBaseDate;
+ date.set(mBaseDate);
+ boolean switchForward;
+ if (deltaX > 0) {
+ date.monthDay -= mNumDays;
+ view.mSelectionDay = mSelectionDay - mNumDays;
+ switchForward = false;
+ } else {
+ date.monthDay += mNumDays;
+ view.mSelectionDay = mSelectionDay + mNumDays;
+ switchForward = true;
+ }
+ date.normalize(true /* ignore isDst */);
+ initView(view);
+ view.setFrame(getLeft(), getTop(), getRight(), getBottom());
+ view.reloadEvents();
+ return switchForward;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ int action = ev.getAction();
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mParentActivity.mGestureDetector.onTouchEvent(ev);
+ return true;
+
+ case MotionEvent.ACTION_MOVE:
+ mParentActivity.mGestureDetector.onTouchEvent(ev);
+ return true;
+
+ case MotionEvent.ACTION_UP:
+ mParentActivity.mGestureDetector.onTouchEvent(ev);
+ if (mOnFlingCalled) {
+ return true;
+ }
+ if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
+ mTouchMode = TOUCH_MODE_INITIAL_STATE;
+ if (Math.abs(mViewStartX) > HORIZONTAL_SCROLL_THRESHOLD) {
+ // The user has gone beyond the threshold so switch views
+ mParentActivity.switchViews(mViewStartX > 0, mViewStartX, mViewWidth);
+ } else {
+ // Not beyond the threshold so invalidate which will cause
+ // the view to snap back. Also call recalc() to ensure
+ // that we have the correct starting date and title.
+ recalc();
+ mTitleTextView.setText(mDateRange);
+ invalidate();
+ }
+ mViewStartX = 0;
+ }
+
+ // If we were scrolling, then reset the selected hour so that it
+ // is visible.
+ if (mScrolling) {
+ mScrolling = false;
+ resetSelectedHour();
+ mRedrawScreen = true;
+ invalidate();
+ }
+ return true;
+
+ // This case isn't expected to happen.
+ case MotionEvent.ACTION_CANCEL:
+ mParentActivity.mGestureDetector.onTouchEvent(ev);
+ mScrolling = false;
+ resetSelectedHour();
+ return true;
+
+ default:
+ if (mParentActivity.mGestureDetector.onTouchEvent(ev)) {
+ return true;
+ }
+ return super.onTouchEvent(ev);
+ }
+ }
+
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
+ MenuItem item;
+
+ // If the trackball is held down, then the context menu pops up and
+ // we never get onKeyUp() for the long-press. So check for it here
+ // and change the selection to the long-press state.
+ if (mSelectionMode != SELECTION_LONGPRESS) {
+ mSelectionMode = SELECTION_LONGPRESS;
+ mRedrawScreen = true;
+ invalidate();
+ }
+
+ final long startMillis = getSelectedTimeInMillis();
+ int flags = DateUtils.FORMAT_SHOW_TIME
+ | DateUtils.FORMAT_CAP_NOON_MIDNIGHT
+ | DateUtils.FORMAT_SHOW_WEEKDAY;
+ final String title = DateUtils.formatDateTime(mParentActivity, startMillis, flags);
+ menu.setHeaderTitle(title);
+
+ int numSelectedEvents = mSelectedEvents.size();
+ if (mNumDays == 1) {
+ // Day view.
+
+ // If there is a selected event, then allow it to be viewed and
+ // edited.
+ if (numSelectedEvents >= 1) {
+ item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_info_details);
+
+ if (isEventEditable(mContext, mSelectedEvent)) {
+ item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_edit);
+ item.setAlphabeticShortcut('e');
+
+ item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_delete);
+ }
+
+ item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_add);
+ item.setAlphabeticShortcut('n');
+ } else {
+ // Otherwise, if the user long-pressed on a blank hour, allow
+ // them to create an event. They can also do this by tapping.
+ item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_add);
+ item.setAlphabeticShortcut('n');
+ }
+ } else {
+ // Week view.
+
+ // If there is a selected event, then allow it to be viewed and
+ // edited.
+ if (numSelectedEvents >= 1) {
+ item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_info_details);
+
+ if (isEventEditable(mContext, mSelectedEvent)) {
+ item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_edit);
+ item.setAlphabeticShortcut('e');
+
+ item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_delete);
+ }
+
+ item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_add);
+ item.setAlphabeticShortcut('n');
+
+ item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_day);
+ item.setAlphabeticShortcut('d');
+
+ item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_agenda);
+ item.setAlphabeticShortcut('a');
+ } else {
+ // No events are selected
+ item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_add);
+ item.setAlphabeticShortcut('n');
+
+ item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_day);
+ item.setAlphabeticShortcut('d');
+
+ item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_agenda);
+ item.setAlphabeticShortcut('a');
+ }
+ }
+
+ mPopup.dismiss();
+ }
+
+ private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener {
+ public boolean onMenuItemClick(MenuItem item) {
+ switch (item.getItemId()) {
+ case MenuHelper.MENU_EVENT_VIEW: {
+ if (mSelectedEvent != null) {
+ long id = mSelectedEvent.id;
+ Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(eventUri);
+ intent.setClassName(mContext, EventInfoActivity.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
+ intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
+ mParentActivity.startActivity(intent);
+ }
+ break;
+ }
+ case MenuHelper.MENU_EVENT_EDIT: {
+ if (mSelectedEvent != null) {
+ long id = mSelectedEvent.id;
+ Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
+ Intent intent = new Intent(Intent.ACTION_EDIT);
+ intent.setData(eventUri);
+ intent.setClassName(mContext, EditEvent.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
+ intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
+ mParentActivity.startActivity(intent);
+ }
+ break;
+ }
+ case MenuHelper.MENU_DAY: {
+ long startMillis = getSelectedTimeInMillis();
+ MenuHelper.switchTo(mParentActivity, DayActivity.class.getName(), startMillis);
+ mParentActivity.finish();
+ break;
+ }
+ case MenuHelper.MENU_AGENDA: {
+ long startMillis = getSelectedTimeInMillis();
+ MenuHelper.switchTo(mParentActivity, AgendaActivity.class.getName(), startMillis);
+ mParentActivity.finish();
+ break;
+ }
+ case MenuHelper.MENU_EVENT_CREATE: {
+ long startMillis = getSelectedTimeInMillis();
+ long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setClassName(mContext, EditEvent.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, startMillis);
+ intent.putExtra(EVENT_END_TIME, endMillis);
+ intent.putExtra(EditEvent.EVENT_ALL_DAY, mSelectionAllDay);
+ mParentActivity.startActivity(intent);
+ break;
+ }
+ case MenuHelper.MENU_EVENT_DELETE: {
+ if (mSelectedEvent != null) {
+ Event selectedEvent = mSelectedEvent;
+ long begin = selectedEvent.startMillis;
+ long end = selectedEvent.endMillis;
+ long id = selectedEvent.id;
+ mDeleteEventHelper.delete(begin, end, id, -1);
+ }
+ break;
+ }
+ default: {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ private static boolean isEventEditable(Context context, Event e) {
+ ContentResolver cr = context.getContentResolver();
+
+ int visibility = Calendars.NO_ACCESS;
+ int relationship = Attendees.RELATIONSHIP_ORGANIZER;
+
+ // Get the calendar id for this event
+ Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id),
+ new String[] { Events.CALENDAR_ID },
+ null /* selection */,
+ null /* selectionArgs */,
+ null /* sort */);
+ if ((cursor == null) || (cursor.getCount() == 0)) {
+ return false;
+ }
+ cursor.moveToFirst();
+ long calId = cursor.getLong(0);
+ cursor.deactivate();
+
+ Uri uri = Calendars.CONTENT_URI;
+ String where = String.format(CALENDARS_WHERE, calId);
+ cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null);
+
+ if (cursor != null) {
+ cursor.moveToFirst();
+ visibility = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL);
+ cursor.close();
+ }
+
+ // Attendees cursor
+ uri = Attendees.CONTENT_URI;
+ where = String.format(ATTENDEES_WHERE, e.id);
+ Cursor attendeesCursor = cr.query(uri, ATTENDEES_PROJECTION, where, null, null);
+ if (attendeesCursor != null) {
+ if (attendeesCursor.moveToFirst()) {
+ relationship = attendeesCursor.getInt(ATTENDEES_INDEX_RELATIONSHIP);
+ }
+ attendeesCursor.close();
+ }
+
+ return visibility >= Calendars.CONTRIBUTOR_ACCESS &&
+ relationship >= Attendees.RELATIONSHIP_ORGANIZER;
+ }
+
+ /**
+ * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position.
+ * If the touch position is not within the displayed grid, then this
+ * method returns false.
+ *
+ * @param x the x position of the touch
+ * @param y the y position of the touch
+ * @return true if the touch position is valid
+ */
+ private boolean setSelectionFromPosition(int x, int y) {
+ if (x < mHoursWidth) {
+ return false;
+ }
+
+ int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP);
+ if (day >= mNumDays) {
+ day = mNumDays - 1;
+ }
+ day += mFirstJulianDay;
+ int hour;
+ if (y < mFirstCell + mFirstHourOffset) {
+ mSelectionAllDay = true;
+ } else {
+ hour = (y - mFirstCell - mFirstHourOffset) / (mCellHeight + HOUR_GAP);
+ hour += mFirstHour;
+ mSelectionHour = hour;
+ mSelectionAllDay = false;
+ }
+ mSelectionDay = day;
+ findSelectedEvent(x, y);
+// Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day
+// + " hour: " + hour
+// + " mFirstCell: " + mFirstCell + " mFirstHourOffset: " + mFirstHourOffset);
+// if (mSelectedEvent != null) {
+// Log.i("Cal", " num events: " + mSelectedEvents.size() + " event: " + mSelectedEvent.title);
+// for (Event ev : mSelectedEvents) {
+// int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
+// | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
+// String timeRange = formatDateRange(mParentActivity,
+// ev.startMillis, ev.endMillis, flags);
+//
+// Log.i("Cal", " " + timeRange + " " + ev.title);
+// }
+// }
+ return true;
+ }
+
+ private void findSelectedEvent(int x, int y) {
+ int date = mSelectionDay;
+ int cellWidth = mCellWidth;
+ ArrayList<Event> events = mEvents;
+ int numEvents = events.size();
+ int left = mHoursWidth + (mSelectionDay - mFirstJulianDay) * (cellWidth + DAY_GAP);
+ int top = 0;
+ mSelectedEvent = null;
+
+ mSelectedEvents.clear();
+ if (mSelectionAllDay) {
+ float yDistance;
+ float minYdistance = 10000.0f; // any large number
+ Event closestEvent = null;
+ float drawHeight = mAllDayHeight;
+ int yOffset = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
+ for (int i = 0; i < numEvents; i++) {
+ Event event = events.get(i);
+ if (!event.allDay) {
+ continue;
+ }
+
+ if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) {
+ float numRectangles = event.getMaxColumns();
+ float height = drawHeight / numRectangles;
+ if (height > MAX_ALLDAY_EVENT_HEIGHT) {
+ height = MAX_ALLDAY_EVENT_HEIGHT;
+ }
+ float eventTop = yOffset + height * event.getColumn();
+ float eventBottom = eventTop + height;
+ if (eventTop < y && eventBottom > y) {
+ // If the touch is inside the event rectangle, then
+ // add the event.
+ mSelectedEvents.add(event);
+ closestEvent = event;
+ break;
+ } else {
+ // Find the closest event
+ if (eventTop >= y) {
+ yDistance = eventTop - y;
+ } else {
+ yDistance = y - eventBottom;
+ }
+ if (yDistance < minYdistance) {
+ minYdistance = yDistance;
+ closestEvent = event;
+ }
+ }
+ }
+ }
+ mSelectedEvent = closestEvent;
+ return;
+ }
+
+ // Adjust y for the scrollable bitmap
+ y += mViewStartY - mFirstCell;
+
+ // Use a region around (x,y) for the selection region
+ Rect region = mRect;
+ region.left = x - 10;
+ region.right = x + 10;
+ region.top = y - 10;
+ region.bottom = y + 10;
+
+ EventGeometry geometry = mEventGeometry;
+
+ for (int i = 0; i < numEvents; i++) {
+ Event event = events.get(i);
+ // Compute the event rectangle.
+ if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
+ continue;
+ }
+
+ // If the event intersects the selection region, then add it to
+ // mSelectedEvents.
+ if (geometry.eventIntersectsSelection(event, region)) {
+ mSelectedEvents.add(event);
+ }
+ }
+
+ // If there are any events in the selected region, then assign the
+ // closest one to mSelectedEvent.
+ if (mSelectedEvents.size() > 0) {
+ int len = mSelectedEvents.size();
+ Event closestEvent = null;
+ float minDist = mViewWidth + mViewHeight; // some large distance
+ for (int index = 0; index < len; index++) {
+ Event ev = mSelectedEvents.get(index);
+ float dist = geometry.pointToEvent(x, y, ev);
+ if (dist < minDist) {
+ minDist = dist;
+ closestEvent = ev;
+ }
+ }
+ mSelectedEvent = closestEvent;
+
+ // Keep the selected hour and day consistent with the selected
+ // event. They could be different if we touched on an empty hour
+ // slot very close to an event in the previous hour slot. In
+ // that case we will select the nearby event.
+ int startDay = mSelectedEvent.startDay;
+ int endDay = mSelectedEvent.endDay;
+ if (mSelectionDay < startDay) {
+ mSelectionDay = startDay;
+ } else if (mSelectionDay > endDay) {
+ mSelectionDay = endDay;
+ }
+
+ int startHour = mSelectedEvent.startTime / 60;
+ int endHour;
+ if (mSelectedEvent.startTime < mSelectedEvent.endTime) {
+ endHour = (mSelectedEvent.endTime - 1) / 60;
+ } else {
+ endHour = mSelectedEvent.endTime / 60;
+ }
+
+ if (mSelectionHour < startHour) {
+ mSelectionHour = startHour;
+ } else if (mSelectionHour > endHour) {
+ mSelectionHour = endHour;
+ }
+ }
+ }
+
+ // Encapsulates the code to continue the scrolling after the
+ // finger is lifted. Instead of stopping the scroll immediately,
+ // the scroll continues to "free spin" and gradually slows down.
+ private class ContinueScroll implements Runnable {
+ int mSignDeltaY;
+ int mAbsDeltaY;
+ float mFloatDeltaY;
+ long mFreeSpinTime;
+ private static final float FRICTION_COEF = 0.7F;
+ private static final long FREE_SPIN_MILLIS = 180;
+ private static final int MAX_DELTA = 60;
+ private static final int SCROLL_REPEAT_INTERVAL = 30;
+
+ public void init(int deltaY) {
+ mSignDeltaY = 0;
+ if (deltaY > 0) {
+ mSignDeltaY = 1;
+ } else if (deltaY < 0) {
+ mSignDeltaY = -1;
+ }
+ mAbsDeltaY = Math.abs(deltaY);
+
+ // Limit the maximum speed
+ if (mAbsDeltaY > MAX_DELTA) {
+ mAbsDeltaY = MAX_DELTA;
+ }
+ mFloatDeltaY = mAbsDeltaY;
+ mFreeSpinTime = System.currentTimeMillis() + FREE_SPIN_MILLIS;
+// Log.i("Cal", "init scroll: mAbsDeltaY: " + mAbsDeltaY
+// + " mViewStartY: " + mViewStartY);
+ }
+
+ public void run() {
+ long time = System.currentTimeMillis();
+
+ // Start out with a frictionless "free spin"
+ if (time > mFreeSpinTime) {
+ // If the delta is small, then apply a fixed deceleration.
+ // Otherwise
+ if (mAbsDeltaY <= 10) {
+ mAbsDeltaY -= 2;
+ } else {
+ mFloatDeltaY *= FRICTION_COEF;
+ mAbsDeltaY = (int) mFloatDeltaY;
+ }
+
+ if (mAbsDeltaY < 0) {
+ mAbsDeltaY = 0;
+ }
+ }
+
+ if (mSignDeltaY == 1) {
+ mViewStartY -= mAbsDeltaY;
+ } else {
+ mViewStartY += mAbsDeltaY;
+ }
+// Log.i("Cal", " scroll: mAbsDeltaY: " + mAbsDeltaY
+// + " mViewStartY: " + mViewStartY);
+
+ if (mViewStartY < 0) {
+ mViewStartY = 0;
+ mAbsDeltaY = 0;
+ } else if (mViewStartY > mMaxViewStartY) {
+ mViewStartY = mMaxViewStartY;
+ mAbsDeltaY = 0;
+ }
+
+ computeFirstHour();
+
+ if (mAbsDeltaY > 0) {
+ postDelayed(this, SCROLL_REPEAT_INTERVAL);
+ } else {
+ // Done scrolling.
+ mScrolling = false;
+ resetSelectedHour();
+ mRedrawScreen = true;
+ }
+
+ invalidate();
+ }
+ }
+
+ /**
+ * Cleanup the pop-up.
+ */
+ public void cleanup() {
+ // Protect against null-pointer exceptions
+ if (mPopup != null) {
+ mPopup.dismiss();
+ }
+ Handler handler = getHandler();
+ if (handler != null) {
+ handler.removeCallbacks(mDismissPopup);
+ }
+ }
+
+ @Override protected void onDetachedFromWindow() {
+ cleanup();
+ if (mBitmap != null) {
+ mBitmap.recycle();
+ mBitmap = null;
+ }
+ super.onDetachedFromWindow();
+ }
+
+ class DismissPopup implements Runnable {
+ public void run() {
+ // Protect against null-pointer exceptions
+ if (mPopup != null) {
+ mPopup.dismiss();
+ }
+ }
+ }
+}
+
diff --git a/src/com/android/calendar/DayActivity.java b/src/com/android/calendar/DayActivity.java
new file mode 100644
index 00000000..51f965b8
--- /dev/null
+++ b/src/com/android/calendar/DayActivity.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2006 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.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.ProgressBar;
+import android.widget.ViewSwitcher;
+
+public class DayActivity extends CalendarActivity implements ViewSwitcher.ViewFactory {
+ /**
+ * The view id used for all the views we create. It's OK to have all child
+ * views have the same ID. This ID is used to pick which view receives
+ * focus when a view hierarchy is saved / restore
+ */
+ private static final int VIEW_ID = 1;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.day_activity);
+
+ mSelectedDay = Utils.timeFromIntent(getIntent());
+ mViewSwitcher = (ViewSwitcher) findViewById(R.id.switcher);
+ mViewSwitcher.setFactory(this);
+ mViewSwitcher.getCurrentView().requestFocus();
+ mProgressBar = (ProgressBar) findViewById(R.id.progress_circular);
+
+ // Record Day View as the (new) default detailed view.
+ String activityString = CalendarApplication.ACTIVITY_NAMES[CalendarApplication.DAY_VIEW_ID];
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(CalendarPreferenceActivity.KEY_DETAILED_VIEW, activityString);
+
+ // Record Day View as the (new) start view
+ editor.putString(CalendarPreferenceActivity.KEY_START_VIEW, activityString);
+ editor.commit();
+ }
+
+ public View makeView() {
+ DayView view = new DayView(this);
+ view.setId(VIEW_ID);
+ view.setLayoutParams(new ViewSwitcher.LayoutParams(
+ LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
+ view.setSelectedDay(mSelectedDay);
+ return view;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ mSelectedDay = view.getSelectedDay();
+ }
+}
diff --git a/src/com/android/calendar/DayView.java b/src/com/android/calendar/DayView.java
new file mode 100644
index 00000000..a24b51b5
--- /dev/null
+++ b/src/com/android/calendar/DayView.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2006 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;
+
+
+public class DayView extends CalendarView {
+ private static final int CELL_MARGIN = 10;
+
+ public DayView(CalendarActivity activity) {
+ super(activity);
+ init();
+ }
+
+ private void init() {
+ mDrawTextInEventRect = true;
+ mNumDays = 1;
+ mEventGeometry.setCellMargin(CELL_MARGIN);
+ }
+}
diff --git a/src/com/android/calendar/DeleteEventHelper.java b/src/com/android/calendar/DeleteEventHelper.java
new file mode 100644
index 00000000..90cd3ff5
--- /dev/null
+++ b/src/com/android/calendar/DeleteEventHelper.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2008 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.app.Activity;
+import android.app.AlertDialog;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.DialogInterface;
+import android.database.Cursor;
+import android.net.Uri;
+import android.pim.EventRecurrence;
+import android.provider.Calendar;
+import android.provider.Calendar.Events;
+import android.text.format.Time;
+import android.widget.Button;
+
+/**
+ * A helper class for deleting events. If a normal event is selected for
+ * deletion, then this pops up a confirmation dialog. If the user confirms,
+ * then the normal event is deleted.
+ *
+ * <p>
+ * If a repeating event is selected for deletion, then this pops up dialog
+ * asking if the user wants to delete just this one instance, or all the
+ * events in the series, or this event plus all following events. The user
+ * may also cancel the delete.
+ * </p>
+ *
+ * <p>
+ * To use this class, create an instance, passing in the parent activity
+ * and a boolean that determines if the parent activity should exit if the
+ * event is deleted. Then to use the instance, call one of the
+ * {@link delete()} methods on this class.
+ *
+ * An instance of this class may be created once and reused (by calling
+ * {@link #delete()} multiple times).
+ */
+public class DeleteEventHelper {
+
+ private static final String TAG = "DeleteEventHelper";
+ private final Activity mParent;
+ private final ContentResolver mContentResolver;
+
+ private long mStartMillis;
+ private long mEndMillis;
+ private Cursor mCursor;
+
+ /**
+ * If true, then call finish() on the parent activity when done.
+ */
+ private boolean mExitWhenDone;
+
+ /**
+ * These are the corresponding indices into the array of strings
+ * "R.array.delete_repeating_labels" in the resource file.
+ */
+ static final int DELETE_SELECTED = 0;
+ static final int DELETE_ALL_FOLLOWING = 1;
+ static final int DELETE_ALL = 2;
+
+ private int mWhichDelete;
+ private AlertDialog mAlertDialog;
+
+ private static final String[] EVENT_PROJECTION = new String[] {
+ Events._ID,
+ Events.TITLE,
+ Events.ALL_DAY,
+ Events.CALENDAR_ID,
+ Events.RRULE,
+ Events.DTSTART,
+ Events._SYNC_ID,
+ Events.EVENT_TIMEZONE,
+ };
+
+ private int mEventIndexId;
+ private int mEventIndexRrule;
+ private String mSyncId;
+
+ public DeleteEventHelper(Activity parent, boolean exitWhenDone) {
+ mParent = parent;
+ mContentResolver = mParent.getContentResolver();
+ mExitWhenDone = exitWhenDone;
+ }
+
+ public void setExitWhenDone(boolean exitWhenDone) {
+ mExitWhenDone = exitWhenDone;
+ }
+
+ /**
+ * This callback is used when a normal event is deleted.
+ */
+ private DialogInterface.OnClickListener mDeleteNormalDialogListener =
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int button) {
+ long id = mCursor.getInt(mEventIndexId);
+ Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
+ mContentResolver.delete(uri, null /* where */, null /* selectionArgs */);
+ if (mExitWhenDone) {
+ mParent.finish();
+ }
+ }
+ };
+
+ /**
+ * This callback is used when a list item for a repeating event is selected
+ */
+ private DialogInterface.OnClickListener mDeleteListListener =
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int button) {
+ mWhichDelete = button;
+
+ // Enable the "ok" button now that the user has selected which
+ // events in the series to delete.
+ Button ok = mAlertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ ok.setEnabled(true);
+ }
+ };
+
+ /**
+ * This callback is used when a repeating event is deleted.
+ */
+ private DialogInterface.OnClickListener mDeleteRepeatingDialogListener =
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int button) {
+ if (mWhichDelete != -1) {
+ deleteRepeatingEvent(mWhichDelete);
+ }
+ }
+ };
+
+ /**
+ * Does the required processing for deleting an event, which includes
+ * first popping up a dialog asking for confirmation (if the event is
+ * a normal event) or a dialog asking which events to delete (if the
+ * event is a repeating event). The "which" parameter is used to check
+ * the initial selection and is only used for repeating events. Set
+ * "which" to -1 to have nothing selected initially.
+ *
+ * @param begin the begin time of the event, in UTC milliseconds
+ * @param end the end time of the event, in UTC milliseconds
+ * @param eventId the event id
+ * @param which one of the values {@link DELETE_SELECTED},
+ * {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1
+ */
+ public void delete(long begin, long end, long eventId, int which) {
+ Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, eventId);
+ Cursor cursor = mParent.managedQuery(uri, EVENT_PROJECTION, null, null);
+ if (cursor == null) {
+ return;
+ }
+ cursor.moveToFirst();
+ delete(begin, end, cursor, which);
+ }
+
+ /**
+ * Does the required processing for deleting an event. This method
+ * takes a {@link Cursor} object as a parameter, which must point to
+ * a row in the Events table containing the required database fields.
+ * The required fields for a normal event are:
+ *
+ * <ul>
+ * <li> Events._ID </li>
+ * <li> Events.TITLE </li>
+ * <li> Events.RRULE </li>
+ * </ul>
+ *
+ * The required fields for a repeating event include the above plus the
+ * following fields:
+ *
+ * <ul>
+ * <li> Events.ALL_DAY </li>
+ * <li> Events.CALENDAR_ID </li>
+ * <li> Events.DTSTART </li>
+ * <li> Events._SYNC_ID </li>
+ * <li> Events.EVENT_TIMEZONE </li>
+ * </ul>
+ *
+ * @param begin the begin time of the event, in UTC milliseconds
+ * @param end the end time of the event, in UTC milliseconds
+ * @param cursor the database cursor containing the required fields
+ * @param which one of the values {@link DELETE_SELECTED},
+ * {@link DELETE_ALL_FOLLOWING}, {@link DELETE_ALL}, or -1
+ */
+ public void delete(long begin, long end, Cursor cursor, int which) {
+ mWhichDelete = which;
+ mStartMillis = begin;
+ mEndMillis = end;
+ mCursor = cursor;
+ mEventIndexId = mCursor.getColumnIndexOrThrow(Events._ID);
+ mEventIndexRrule = mCursor.getColumnIndexOrThrow(Events.RRULE);
+ int eventIndexSyncId = mCursor.getColumnIndexOrThrow(Events._SYNC_ID);
+ mSyncId = mCursor.getString(eventIndexSyncId);
+
+ // If this is a repeating event, then pop up a dialog asking the
+ // user if they want to delete all of the repeating events or
+ // just some of them.
+ String rRule = mCursor.getString(mEventIndexRrule);
+ if (rRule == null) {
+ // This is a normal event. Pop up a confirmation dialog.
+ new AlertDialog.Builder(mParent)
+ .setTitle(R.string.delete_title)
+ .setMessage(R.string.delete_this_event_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.ok, mDeleteNormalDialogListener)
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ } else {
+ // This is a repeating event. Pop up a dialog asking which events
+ // to delete.
+ int labelsArrayId = R.array.delete_repeating_labels;
+ if (mSyncId == null) {
+ labelsArrayId = R.array.delete_repeating_labels_no_selected;
+ }
+ AlertDialog dialog = new AlertDialog.Builder(mParent)
+ .setTitle(R.string.delete_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setSingleChoiceItems(labelsArrayId, which, mDeleteListListener)
+ .setPositiveButton(android.R.string.ok, mDeleteRepeatingDialogListener)
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ mAlertDialog = dialog;
+
+ // Disable the "Ok" button until the user selects which events to
+ // delete.
+ Button ok = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ ok.setEnabled(false);
+ }
+ }
+
+ private void deleteRepeatingEvent(int which) {
+ int indexDtstart = mCursor.getColumnIndexOrThrow(Events.DTSTART);
+ int indexAllDay = mCursor.getColumnIndexOrThrow(Events.ALL_DAY);
+ int indexTitle = mCursor.getColumnIndexOrThrow(Events.TITLE);
+ int indexTimezone = mCursor.getColumnIndexOrThrow(Events.EVENT_TIMEZONE);
+ int indexCalendarId = mCursor.getColumnIndexOrThrow(Events.CALENDAR_ID);
+
+ String rRule = mCursor.getString(mEventIndexRrule);
+ boolean allDay = mCursor.getInt(indexAllDay) != 0;
+ long dtstart = mCursor.getLong(indexDtstart);
+ long id = mCursor.getInt(mEventIndexId);
+
+ // If the repeating event has not been given a sync id from the server
+ // yet, then we can't delete a single instance of this event. (This is
+ // a deficiency in the CalendarProvider and sync code.) We checked for
+ // that when creating the list of items in the dialog and we removed
+ // the first element ("DELETE_SELECTED") from the dialog in that case.
+ // The "which" value is a 0-based index into the list of items, where
+ // the "DELETE_SELECTED" item is at index 0.
+ if (mSyncId == null) {
+ which += 1;
+ }
+
+ switch (which) {
+ case DELETE_SELECTED:
+ {
+ // If we are deleting the first event in the series, then
+ // instead of creating a recurrence exception, just change
+ // the start time of the recurrence.
+ if (dtstart == mStartMillis) {
+ // TODO
+ }
+
+ // Create a recurrence exception by creating a new event
+ // with the status "cancelled".
+ ContentValues values = new ContentValues();
+
+ // The title might not be necessary, but it makes it easier
+ // to find this entry in the database when there is a problem.
+ String title = mCursor.getString(indexTitle);
+ values.put(Events.TITLE, title);
+
+ String timezone = mCursor.getString(indexTimezone);
+ int calendarId = mCursor.getInt(indexCalendarId);
+ values.put(Events.EVENT_TIMEZONE, timezone);
+ values.put(Events.ALL_DAY, allDay ? 1 : 0);
+ values.put(Events.CALENDAR_ID, calendarId);
+ values.put(Events.DTSTART, mStartMillis);
+ values.put(Events.DTEND, mEndMillis);
+ values.put(Events.ORIGINAL_EVENT, mSyncId);
+ values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
+ values.put(Events.STATUS, Events.STATUS_CANCELED);
+
+ mContentResolver.insert(Events.CONTENT_URI, values);
+ break;
+ }
+ case DELETE_ALL: {
+ Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
+ mContentResolver.delete(uri, null /* where */, null /* selectionArgs */);
+ break;
+ }
+ case DELETE_ALL_FOLLOWING: {
+ // If we are deleting the first event in the series and all
+ // following events, then delete them all.
+ if (dtstart == mStartMillis) {
+ Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
+ mContentResolver.delete(uri, null /* where */, null /* selectionArgs */);
+ break;
+ }
+
+ // Modify the repeating event to end just before this event time
+ EventRecurrence eventRecurrence = new EventRecurrence();
+ eventRecurrence.parse(rRule);
+ Time date = new Time();
+ if (allDay) {
+ date.timezone = Time.TIMEZONE_UTC;
+ }
+ date.set(mStartMillis);
+ date.second--;
+ date.normalize(false);
+
+ // Google calendar seems to require the UNTIL string to be
+ // in UTC.
+ date.switchTimezone(Time.TIMEZONE_UTC);
+ eventRecurrence.until = date.format2445();
+
+ ContentValues values = new ContentValues();
+ values.put(Events.DTSTART, dtstart);
+ values.put(Events.RRULE, eventRecurrence.toString());
+ Uri uri = ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, id);
+ mContentResolver.update(uri, values, null, null);
+ break;
+ }
+ }
+ if (mExitWhenDone) {
+ mParent.finish();
+ }
+ }
+}
diff --git a/src/com/android/calendar/EditEvent.java b/src/com/android/calendar/EditEvent.java
new file mode 100644
index 00000000..f020a7cc
--- /dev/null
+++ b/src/com/android/calendar/EditEvent.java
@@ -0,0 +1,1690 @@
+/*
+ * Copyright (C) 2008 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 static android.provider.Calendar.EVENT_BEGIN_TIME;
+import static android.provider.Calendar.EVENT_END_TIME;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.DatePickerDialog;
+import android.app.ProgressDialog;
+import android.app.TimePickerDialog;
+import android.app.DatePickerDialog.OnDateSetListener;
+import android.app.TimePickerDialog.OnTimeSetListener;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.pim.EventRecurrence;
+import android.preference.PreferenceManager;
+import android.provider.Calendar.Calendars;
+import android.provider.Calendar.Events;
+import android.provider.Calendar.Reminders;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.DatePicker;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ResourceCursorAdapter;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.TimePicker;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.TimeZone;
+
+public class EditEvent extends Activity implements View.OnClickListener,
+ DialogInterface.OnCancelListener, DialogInterface.OnClickListener {
+ /**
+ * This is the symbolic name for the key used to pass in the boolean
+ * for creating all-day events that is part of the extra data of the intent.
+ * This is used only for creating new events and is set to true if
+ * the default for the new event should be an all-day event.
+ */
+ public static final String EVENT_ALL_DAY = "allDay";
+
+ private static final int MAX_REMINDERS = 5;
+
+ private static final int MENU_GROUP_REMINDER = 1;
+ private static final int MENU_GROUP_SHOW_OPTIONS = 2;
+ private static final int MENU_GROUP_HIDE_OPTIONS = 3;
+
+ private static final int MENU_ADD_REMINDER = 1;
+ private static final int MENU_SHOW_EXTRA_OPTIONS = 2;
+ private static final int MENU_HIDE_EXTRA_OPTIONS = 3;
+
+ private static final String[] EVENT_PROJECTION = new String[] {
+ Events._ID, // 0
+ Events.TITLE, // 1
+ Events.DESCRIPTION, // 2
+ Events.EVENT_LOCATION, // 3
+ Events.ALL_DAY, // 4
+ Events.HAS_ALARM, // 5
+ Events.CALENDAR_ID, // 6
+ Events.DTSTART, // 7
+ Events.DURATION, // 8
+ Events.EVENT_TIMEZONE, // 9
+ Events.RRULE, // 10
+ Events._SYNC_ID, // 11
+ Events.TRANSPARENCY, // 12
+ Events.VISIBILITY, // 13
+ };
+ private static final int EVENT_INDEX_ID = 0;
+ private static final int EVENT_INDEX_TITLE = 1;
+ private static final int EVENT_INDEX_DESCRIPTION = 2;
+ private static final int EVENT_INDEX_EVENT_LOCATION = 3;
+ private static final int EVENT_INDEX_ALL_DAY = 4;
+ private static final int EVENT_INDEX_HAS_ALARM = 5;
+ private static final int EVENT_INDEX_CALENDAR_ID = 6;
+ private static final int EVENT_INDEX_DTSTART = 7;
+ private static final int EVENT_INDEX_DURATION = 8;
+ private static final int EVENT_INDEX_TIMEZONE = 9;
+ private static final int EVENT_INDEX_RRULE = 10;
+ private static final int EVENT_INDEX_SYNC_ID = 11;
+ private static final int EVENT_INDEX_TRANSPARENCY = 12;
+ private static final int EVENT_INDEX_VISIBILITY = 13;
+
+ private static final String[] CALENDARS_PROJECTION = new String[] {
+ Calendars._ID, // 0
+ Calendars.DISPLAY_NAME, // 1
+ Calendars.TIMEZONE, // 2
+ };
+ private static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
+ private static final int CALENDARS_INDEX_TIMEZONE = 2;
+ private static final String CALENDARS_WHERE = Calendars.ACCESS_LEVEL + ">=" +
+ Calendars.CONTRIBUTOR_ACCESS + " AND " + Calendars.SYNC_EVENTS + "=1";
+
+ private static final String[] REMINDERS_PROJECTION = new String[] {
+ Reminders._ID, // 0
+ Reminders.MINUTES, // 1
+ };
+ private static final int REMINDERS_INDEX_MINUTES = 1;
+ private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=%d AND (" +
+ Reminders.METHOD + "=" + Reminders.METHOD_ALERT + " OR " + Reminders.METHOD + "=" +
+ Reminders.METHOD_DEFAULT + ")";
+
+ private static final int DOES_NOT_REPEAT = 0;
+ private static final int REPEATS_DAILY = 1;
+ private static final int REPEATS_EVERY_WEEKDAY = 2;
+ private static final int REPEATS_WEEKLY_ON_DAY = 3;
+ private static final int REPEATS_MONTHLY_ON_DAY_COUNT = 4;
+ private static final int REPEATS_MONTHLY_ON_DAY = 5;
+ private static final int REPEATS_YEARLY = 6;
+ private static final int REPEATS_CUSTOM = 7;
+
+ private static final int MODIFY_UNINITIALIZED = 0;
+ private static final int MODIFY_SELECTED = 1;
+ private static final int MODIFY_ALL = 2;
+ private static final int MODIFY_ALL_FOLLOWING = 3;
+
+ private static final int DAY_IN_SECONDS = 24 * 60 * 60;
+
+ private int mFirstDayOfWeek; // cached in onCreate
+ private Uri mUri;
+ private Cursor mEventCursor;
+ private Cursor mCalendarsCursor;
+
+ private Button mStartDateButton;
+ private Button mEndDateButton;
+ private Button mStartTimeButton;
+ private Button mEndTimeButton;
+ private Button mSaveButton;
+ private Button mDeleteButton;
+ private Button mDiscardButton;
+ private CheckBox mAllDayCheckBox;
+ private Spinner mCalendarsSpinner;
+ private Spinner mRepeatsSpinner;
+ private Spinner mAvailabilitySpinner;
+ private Spinner mVisibilitySpinner;
+ private TextView mTitleTextView;
+ private TextView mLocationTextView;
+ private TextView mDescriptionTextView;
+ private View mRemindersSeparator;
+ private LinearLayout mRemindersContainer;
+ private LinearLayout mExtraOptions;
+ private ArrayList<Integer> mOriginalMinutes = new ArrayList<Integer>();
+ private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0);
+
+ private EventRecurrence mEventRecurrence = new EventRecurrence();
+ private String mRrule;
+ private boolean mCalendarsQueryComplete;
+ private boolean mSaveAfterQueryComplete;
+ private ProgressDialog mLoadingCalendarsDialog;
+ private AlertDialog mNoCalendarsDialog;
+ private ContentValues mInitialValues;
+
+ /**
+ * If the repeating event is created on the phone and it hasn't been
+ * synced yet to the web server, then there is a bug where you can't
+ * delete or change an instance of the repeating event. This case
+ * can be detected with mSyncId. If mSyncId == null, then the repeating
+ * event has not been synced to the phone, in which case we won't allow
+ * the user to change one instance.
+ */
+ private String mSyncId;
+
+ private ArrayList<Integer> mRecurrenceIndexes = new ArrayList<Integer> (0);
+ private ArrayList<Integer> mReminderValues;
+ private ArrayList<String> mReminderLabels;
+
+ private Time mStartTime;
+ private Time mEndTime;
+ private int mModification = MODIFY_UNINITIALIZED;
+ private int mDefaultReminderMinutes;
+
+ private DeleteEventHelper mDeleteEventHelper;
+ private QueryHandler mQueryHandler;
+
+ /* This class is used to update the time buttons. */
+ private class TimeListener implements OnTimeSetListener {
+ private View mView;
+
+ public TimeListener(View view) {
+ mView = view;
+ }
+
+ public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
+ // Cache the member variables locally to avoid inner class overhead.
+ Time startTime = mStartTime;
+ Time endTime = mEndTime;
+
+ // Cache the start and end millis so that we limit the number
+ // of calls to normalize() and toMillis(), which are fairly
+ // expensive.
+ long startMillis;
+ long endMillis;
+ if (mView == mStartTimeButton) {
+ // The start time was changed.
+ int hourDuration = endTime.hour - startTime.hour;
+ int minuteDuration = endTime.minute - startTime.minute;
+
+ startTime.hour = hourOfDay;
+ startTime.minute = minute;
+ startMillis = startTime.normalize(true);
+
+ // Also update the end time to keep the duration constant.
+ endTime.hour = hourOfDay + hourDuration;
+ endTime.minute = minute + minuteDuration;
+ endMillis = endTime.normalize(true);
+ } else {
+ // The end time was changed.
+ startMillis = startTime.toMillis(true);
+ endTime.hour = hourOfDay;
+ endTime.minute = minute;
+ endMillis = endTime.normalize(true);
+
+ // Do not allow an event to have an end time before the start time.
+ if (endTime.before(startTime)) {
+ endTime.set(startTime);
+ endMillis = startMillis;
+ }
+ }
+
+ setDate(mEndDateButton, endMillis);
+ setTime(mStartTimeButton, startMillis);
+ setTime(mEndTimeButton, endMillis);
+ }
+ }
+
+ private class TimeClickListener implements View.OnClickListener {
+ private Time mTime;
+
+ public TimeClickListener(Time time) {
+ mTime = time;
+ }
+
+ public void onClick(View v) {
+ new TimePickerDialog(EditEvent.this, new TimeListener(v),
+ mTime.hour, mTime.minute,
+ DateFormat.is24HourFormat(EditEvent.this)).show();
+ }
+ }
+
+ private class DateListener implements OnDateSetListener {
+ View mView;
+
+ public DateListener(View view) {
+ mView = view;
+ }
+
+ public void onDateSet(DatePicker view, int year, int month, int monthDay) {
+ // Cache the member variables locally to avoid inner class overhead.
+ Time startTime = mStartTime;
+ Time endTime = mEndTime;
+
+ // Cache the start and end millis so that we limit the number
+ // of calls to normalize() and toMillis(), which are fairly
+ // expensive.
+ long startMillis;
+ long endMillis;
+ if (mView == mStartDateButton) {
+ // The start date was changed.
+ int yearDuration = endTime.year - startTime.year;
+ int monthDuration = endTime.month - startTime.month;
+ int monthDayDuration = endTime.monthDay - startTime.monthDay;
+
+ startTime.year = year;
+ startTime.month = month;
+ startTime.monthDay = monthDay;
+ startMillis = startTime.normalize(true);
+
+ // Also update the end date to keep the duration constant.
+ endTime.year = year + yearDuration;
+ endTime.month = month + monthDuration;
+ endTime.monthDay = monthDay + monthDayDuration;
+ endMillis = endTime.normalize(true);
+
+ // If the start date has changed then update the repeats.
+ populateRepeats();
+ } else {
+ // The end date was changed.
+ startMillis = startTime.toMillis(true);
+ endTime.year = year;
+ endTime.month = month;
+ endTime.monthDay = monthDay;
+ endMillis = endTime.normalize(true);
+
+ // Do not allow an event to have an end time before the start time.
+ if (endTime.before(startTime)) {
+ endTime.set(startTime);
+ endMillis = startMillis;
+ }
+ }
+
+ setDate(mStartDateButton, startMillis);
+ setDate(mEndDateButton, endMillis);
+ setTime(mEndTimeButton, endMillis); // In case end time had to be reset
+ }
+ }
+
+ private class DateClickListener implements View.OnClickListener {
+ private Time mTime;
+
+ public DateClickListener(Time time) {
+ mTime = time;
+ }
+
+ public void onClick(View v) {
+ new DatePickerDialog(EditEvent.this, new DateListener(v), mTime.year,
+ mTime.month, mTime.monthDay).show();
+ }
+ }
+
+ private class CalendarsAdapter extends ResourceCursorAdapter {
+ public CalendarsAdapter(Context context, Cursor c) {
+ super(context, R.layout.calendars_item, c);
+ setDropDownViewResource(R.layout.calendars_dropdown_item);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ TextView name = (TextView) view.findViewById(R.id.calendar_name);
+ name.setText(cursor.getString(CALENDARS_INDEX_DISPLAY_NAME));
+ }
+ }
+
+ // This is called if the user clicks on one of the buttons: "Save",
+ // "Discard", or "Delete". This is also called if the user clicks
+ // on the "remove reminder" button.
+ public void onClick(View v) {
+ if (v == mSaveButton) {
+ if (save()) {
+ finish();
+ }
+ return;
+ }
+
+ if (v == mDeleteButton) {
+ long begin = mStartTime.toMillis(false /* use isDst */);
+ long end = mEndTime.toMillis(false /* use isDst */);
+ int which = -1;
+ switch (mModification) {
+ case MODIFY_SELECTED:
+ which = DeleteEventHelper.DELETE_SELECTED;
+ break;
+ case MODIFY_ALL_FOLLOWING:
+ which = DeleteEventHelper.DELETE_ALL_FOLLOWING;
+ break;
+ case MODIFY_ALL:
+ which = DeleteEventHelper.DELETE_ALL;
+ break;
+ }
+ mDeleteEventHelper.delete(begin, end, mEventCursor, which);
+ return;
+ }
+
+ if (v == mDiscardButton) {
+ finish();
+ return;
+ }
+
+ // This must be a click on one of the "remove reminder" buttons
+ LinearLayout reminderItem = (LinearLayout) v.getParent();
+ LinearLayout parent = (LinearLayout) reminderItem.getParent();
+ parent.removeView(reminderItem);
+ mReminderItems.remove(reminderItem);
+ updateRemindersVisibility();
+ }
+
+ // This is called if the user cancels a popup dialog. There are two
+ // dialogs: the "Loading calendars" dialog, and the "No calendars"
+ // dialog. The "Loading calendars" dialog is shown if there is a delay
+ // in loading the calendars (needed when creating an event) and the user
+ // tries to save the event before the calendars have finished loading.
+ // The "No calendars" dialog is shown if there are no syncable calendars.
+ public void onCancel(DialogInterface dialog) {
+ if (dialog == mLoadingCalendarsDialog) {
+ mSaveAfterQueryComplete = false;
+ } else if (dialog == mNoCalendarsDialog) {
+ finish();
+ }
+ }
+
+ // This is called if the user clicks on a dialog button.
+ public void onClick(DialogInterface dialog, int which) {
+ if (dialog == mNoCalendarsDialog) {
+ finish();
+ }
+ }
+
+ private class QueryHandler extends AsyncQueryHandler {
+ public QueryHandler(ContentResolver cr) {
+ super(cr);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ // If the Activity is finishing, then close the cursor.
+ // Otherwise, use the new cursor in the adapter.
+ if (isFinishing()) {
+ stopManagingCursor(cursor);
+ cursor.close();
+ } else {
+ mCalendarsCursor = cursor;
+ startManagingCursor(cursor);
+
+ // Stop the spinner
+ getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
+ Window.PROGRESS_VISIBILITY_OFF);
+
+ // If there are no syncable calendars, then we cannot allow
+ // creating a new event.
+ if (cursor.getCount() == 0) {
+ // Cancel the "loading calendars" dialog if it exists
+ if (mSaveAfterQueryComplete) {
+ mLoadingCalendarsDialog.cancel();
+ }
+
+ // Create an error message for the user that, when clicked,
+ // will exit this activity without saving the event.
+ AlertDialog.Builder builder = new AlertDialog.Builder(EditEvent.this);
+ builder.setTitle(R.string.no_syncable_calendars)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.no_calendars_found)
+ .setPositiveButton(android.R.string.ok, EditEvent.this)
+ .setOnCancelListener(EditEvent.this);
+ mNoCalendarsDialog = builder.show();
+ return;
+ }
+
+ // populate the calendars spinner
+ CalendarsAdapter adapter = new CalendarsAdapter(EditEvent.this, mCalendarsCursor);
+ mCalendarsSpinner.setAdapter(adapter);
+ mCalendarsQueryComplete = true;
+ if (mSaveAfterQueryComplete) {
+ mLoadingCalendarsDialog.cancel();
+ save();
+ finish();
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ setContentView(R.layout.edit_event);
+
+ mFirstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek();
+
+ mStartTime = new Time();
+ mEndTime = new Time();
+
+ Intent intent = getIntent();
+ mUri = intent.getData();
+
+ if (mUri != null) {
+ mEventCursor = managedQuery(mUri, EVENT_PROJECTION, null, null);
+ if (mEventCursor == null || mEventCursor.getCount() == 0) {
+ // The cursor is empty. This can happen if the event was deleted.
+ finish();
+ return;
+ }
+ }
+
+ long begin = intent.getLongExtra(EVENT_BEGIN_TIME, 0);
+ long end = intent.getLongExtra(EVENT_END_TIME, 0);
+
+ boolean allDay = false;
+ if (mEventCursor != null) {
+ // The event already exists so fetch the all-day status
+ mEventCursor.moveToFirst();
+ allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
+ String rrule = mEventCursor.getString(EVENT_INDEX_RRULE);
+ String timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
+ long calendarId = mEventCursor.getInt(EVENT_INDEX_CALENDAR_ID);
+
+ // Remember the initial values
+ mInitialValues = new ContentValues();
+ mInitialValues.put(EVENT_BEGIN_TIME, begin);
+ mInitialValues.put(EVENT_END_TIME, end);
+ mInitialValues.put(Events.ALL_DAY, allDay ? 1 : 0);
+ mInitialValues.put(Events.RRULE, rrule);
+ mInitialValues.put(Events.EVENT_TIMEZONE, timezone);
+ mInitialValues.put(Events.CALENDAR_ID, calendarId);
+ } else {
+ // We are creating a new event, so set the default from the
+ // intent (if specified).
+ allDay = intent.getBooleanExtra(EVENT_ALL_DAY, false);
+
+ // Start the spinner
+ getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
+ Window.PROGRESS_VISIBILITY_ON);
+
+ // Start a query in the background to read the list of calendars
+ mQueryHandler = new QueryHandler(getContentResolver());
+ mQueryHandler.startQuery(0, null, Calendars.CONTENT_URI, CALENDARS_PROJECTION,
+ CALENDARS_WHERE, null /* selection args */, null /* sort order */);
+ }
+
+ // If the event is all-day, read the times in UTC timezone
+ if (begin != 0) {
+ if (allDay) {
+ String tz = mStartTime.timezone;
+ mStartTime.timezone = Time.TIMEZONE_UTC;
+ mStartTime.set(begin);
+ mStartTime.timezone = tz;
+
+ // Calling normalize to calculate isDst
+ mStartTime.normalize(true);
+ } else {
+ mStartTime.set(begin);
+ }
+ }
+
+ if (end != 0) {
+ if (allDay) {
+ String tz = mStartTime.timezone;
+ mEndTime.timezone = Time.TIMEZONE_UTC;
+ mEndTime.set(end);
+ mEndTime.timezone = tz;
+
+ // Calling normalize to calculate isDst
+ mEndTime.normalize(true);
+ } else {
+ mEndTime.set(end);
+ }
+ }
+
+ // cache all the widgets
+ mTitleTextView = (TextView) findViewById(R.id.title);
+ mLocationTextView = (TextView) findViewById(R.id.location);
+ mDescriptionTextView = (TextView) findViewById(R.id.description);
+ mStartDateButton = (Button) findViewById(R.id.start_date);
+ mEndDateButton = (Button) findViewById(R.id.end_date);
+ mStartTimeButton = (Button) findViewById(R.id.start_time);
+ mEndTimeButton = (Button) findViewById(R.id.end_time);
+ mAllDayCheckBox = (CheckBox) findViewById(R.id.is_all_day);
+ mCalendarsSpinner = (Spinner) findViewById(R.id.calendars);
+ mRepeatsSpinner = (Spinner) findViewById(R.id.repeats);
+ mAvailabilitySpinner = (Spinner) findViewById(R.id.availability);
+ mVisibilitySpinner = (Spinner) findViewById(R.id.visibility);
+ mRemindersSeparator = findViewById(R.id.reminders_separator);
+ mRemindersContainer = (LinearLayout) findViewById(R.id.reminders_container);
+ mExtraOptions = (LinearLayout) findViewById(R.id.extra_options_container);
+
+ mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (isChecked) {
+ if (mEndTime.hour == 0 && mEndTime.minute == 0) {
+ mEndTime.monthDay--;
+ long endMillis = mEndTime.normalize(true);
+
+ // Do not allow an event to have an end time before the start time.
+ if (mEndTime.before(mStartTime)) {
+ mEndTime.set(mStartTime);
+ endMillis = mEndTime.normalize(true);
+ }
+ setDate(mEndDateButton, endMillis);
+ setTime(mEndTimeButton, endMillis);
+ }
+
+ mStartTimeButton.setVisibility(View.GONE);
+ mEndTimeButton.setVisibility(View.GONE);
+ } else {
+ if (mEndTime.hour == 0 && mEndTime.minute == 0) {
+ mEndTime.monthDay++;
+ long endMillis = mEndTime.normalize(true);
+ setDate(mEndDateButton, endMillis);
+ setTime(mEndTimeButton, endMillis);
+ }
+
+ mStartTimeButton.setVisibility(View.VISIBLE);
+ mEndTimeButton.setVisibility(View.VISIBLE);
+ }
+ }
+ });
+
+ if (allDay) {
+ mAllDayCheckBox.setChecked(true);
+ } else {
+ mAllDayCheckBox.setChecked(false);
+ }
+
+ mSaveButton = (Button) findViewById(R.id.save);
+ mSaveButton.setOnClickListener(this);
+
+ mDeleteButton = (Button) findViewById(R.id.delete);
+ mDeleteButton.setOnClickListener(this);
+
+ mDiscardButton = (Button) findViewById(R.id.discard);
+ mDiscardButton.setOnClickListener(this);
+
+ // Initialize the reminder values array.
+ Resources r = getResources();
+ String[] strings = r.getStringArray(R.array.reminder_minutes_values);
+ int size = strings.length;
+ ArrayList<Integer> list = new ArrayList<Integer>(size);
+ for (int i = 0 ; i < size ; i++) {
+ list.add(Integer.parseInt(strings[i]));
+ }
+ mReminderValues = list;
+ String[] labels = r.getStringArray(R.array.reminder_minutes_labels);
+ mReminderLabels = new ArrayList<String>(Arrays.asList(labels));
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ String durationString =
+ prefs.getString(CalendarPreferenceActivity.KEY_DEFAULT_REMINDER, "0");
+ mDefaultReminderMinutes = Integer.parseInt(durationString);
+
+ // Reminders cursor
+ boolean hasAlarm = (mEventCursor != null)
+ && (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0);
+ if (hasAlarm) {
+ Uri uri = Reminders.CONTENT_URI;
+ long eventId = mEventCursor.getLong(EVENT_INDEX_ID);
+ String where = String.format(REMINDERS_WHERE, eventId);
+ ContentResolver cr = getContentResolver();
+ Cursor reminderCursor = cr.query(uri, REMINDERS_PROJECTION, where, null, null);
+ try {
+ // First pass: collect all the custom reminder minutes (e.g.,
+ // a reminder of 8 minutes) into a global list.
+ while (reminderCursor.moveToNext()) {
+ int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
+ EditEvent.addMinutesToList(this, mReminderValues, mReminderLabels, minutes);
+ }
+
+ // Second pass: create the reminder spinners
+ reminderCursor.moveToPosition(-1);
+ while (reminderCursor.moveToNext()) {
+ int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
+ mOriginalMinutes.add(minutes);
+ EditEvent.addReminder(this, this, mReminderItems, mReminderValues,
+ mReminderLabels, minutes);
+ }
+ } finally {
+ reminderCursor.close();
+ }
+ }
+ updateRemindersVisibility();
+
+ mDeleteEventHelper = new DeleteEventHelper(this, true /* exit when done */);
+
+ if (mEventCursor == null) {
+ // Allow the intent to specify the fields in the event.
+ // This will allow other apps to create events easily.
+ initFromIntent(intent);
+ }
+ }
+
+ private void initFromIntent(Intent intent) {
+ String title = intent.getStringExtra(Events.TITLE);
+ if (title != null) {
+ mTitleTextView.setText(title);
+ }
+
+ String location = intent.getStringExtra(Events.EVENT_LOCATION);
+ if (location != null) {
+ mLocationTextView.setText(location);
+ }
+
+ String description = intent.getStringExtra(Events.DESCRIPTION);
+ if (description != null) {
+ mDescriptionTextView.setText(description);
+ }
+
+ int availability = intent.getIntExtra(Events.TRANSPARENCY, -1);
+ if (availability != -1) {
+ mAvailabilitySpinner.setSelection(availability);
+ }
+
+ int visibility = intent.getIntExtra(Events.VISIBILITY, -1);
+ if (visibility != -1) {
+ mVisibilitySpinner.setSelection(visibility);
+ }
+
+ String rrule = intent.getStringExtra(Events.RRULE);
+ if (rrule != null) {
+ mRrule = rrule;
+ mEventRecurrence.parse(rrule);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (mUri != null) {
+ if (mEventCursor == null || mEventCursor.getCount() == 0) {
+ // The cursor is empty. This can happen if the event was deleted.
+ finish();
+ return;
+ }
+ }
+
+ if (mEventCursor != null) {
+ Cursor cursor = mEventCursor;
+ cursor.moveToFirst();
+
+ mRrule = cursor.getString(EVENT_INDEX_RRULE);
+ String title = cursor.getString(EVENT_INDEX_TITLE);
+ String description = cursor.getString(EVENT_INDEX_DESCRIPTION);
+ String location = cursor.getString(EVENT_INDEX_EVENT_LOCATION);
+ int availability = cursor.getInt(EVENT_INDEX_TRANSPARENCY);
+ int visibility = cursor.getInt(EVENT_INDEX_VISIBILITY);
+ if (visibility > 0) {
+ // For now we the array contains the values 0, 2, and 3. We subtract one to match.
+ visibility--;
+ }
+
+ if (!TextUtils.isEmpty(mRrule) && mModification == MODIFY_UNINITIALIZED) {
+ // If this event has not been synced, then don't allow deleting
+ // or changing a single instance.
+ mSyncId = cursor.getString(EVENT_INDEX_SYNC_ID);
+ mEventRecurrence.parse(mRrule);
+
+ // If we haven't synced this repeating event yet, then don't
+ // allow the user to change just one instance.
+ int itemIndex = 0;
+ CharSequence[] items;
+ if (mSyncId == null) {
+ items = new CharSequence[2];
+ } else {
+ items = new CharSequence[3];
+ items[itemIndex++] = getText(R.string.modify_event);
+ }
+ items[itemIndex++] = getText(R.string.modify_all);
+ items[itemIndex++] = getText(R.string.modify_all_following);
+
+ // Display the modification dialog.
+ new AlertDialog.Builder(this)
+ .setOnCancelListener(new OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }
+ })
+ .setTitle(R.string.edit_event_label)
+ .setItems(items, new OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == 0) {
+ mModification =
+ (mSyncId == null) ? MODIFY_ALL : MODIFY_SELECTED;
+ } else if (which == 1) {
+ mModification =
+ (mSyncId == null) ? MODIFY_ALL_FOLLOWING : MODIFY_ALL;
+ } else if (which == 2) {
+ mModification = MODIFY_ALL_FOLLOWING;
+ }
+
+ // If we are modifying all the events in a
+ // series then disable and ignore the date.
+ if (mModification == MODIFY_ALL) {
+ mStartDateButton.setEnabled(false);
+ mEndDateButton.setEnabled(false);
+ } else if (mModification == MODIFY_SELECTED) {
+ mRepeatsSpinner.setEnabled(false);
+ } else {
+ // We could allow changing the Rrule for
+ // all following instances but we'll
+ // keep it simple for now.
+ mRepeatsSpinner.setEnabled(false);
+ }
+ }
+ })
+ .show();
+ }
+
+ mTitleTextView.setText(title);
+ mLocationTextView.setText(location);
+ mDescriptionTextView.setText(description);
+ mAvailabilitySpinner.setSelection(availability);
+ mVisibilitySpinner.setSelection(visibility);
+
+ // This is an existing event so hide the calendar spinner
+ // since we can't change the calendar.
+ View calendarSeparator = findViewById(R.id.calendar_separator);
+ View calendarLabel = findViewById(R.id.calendar_label);
+ calendarSeparator.setVisibility(View.GONE);
+ calendarLabel.setVisibility(View.GONE);
+ mCalendarsSpinner.setVisibility(View.GONE);
+ } else if (Time.isEpoch(mStartTime) && Time.isEpoch(mEndTime)) {
+ mStartTime.setToNow();
+
+ // Round the time to the nearest half hour.
+ mStartTime.second = 0;
+ int minute = mStartTime.minute;
+ if (minute > 0 && minute <= 30) {
+ mStartTime.minute = 30;
+ } else {
+ mStartTime.minute = 0;
+ mStartTime.hour += 1;
+ }
+
+ long startMillis = mStartTime.normalize(true /* ignore isDst */);
+ mEndTime.set(startMillis + DateUtils.HOUR_IN_MILLIS);
+ } else {
+ // New event - set the default reminder
+ if (mDefaultReminderMinutes != 0) {
+ addReminder(this, this, mReminderItems, mReminderValues,
+ mReminderLabels, mDefaultReminderMinutes);
+ }
+
+ // Hide delete button
+ mDeleteButton.setVisibility(View.GONE);
+ }
+
+ updateRemindersVisibility();
+ populateWhen();
+ populateRepeats();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuItem item;
+ item = menu.add(MENU_GROUP_REMINDER, MENU_ADD_REMINDER, 0,
+ R.string.add_new_reminder);
+ item.setIcon(R.drawable.ic_menu_reminder);
+ item.setAlphabeticShortcut('r');
+
+ item = menu.add(MENU_GROUP_SHOW_OPTIONS, MENU_SHOW_EXTRA_OPTIONS, 0,
+ R.string.edit_event_show_extra_options);
+ item.setIcon(R.drawable.ic_menu_show_list);
+ item = menu.add(MENU_GROUP_HIDE_OPTIONS, MENU_HIDE_EXTRA_OPTIONS, 0,
+ R.string.edit_event_hide_extra_options);
+ item.setIcon(R.drawable.ic_menu_show_list);
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ if (mReminderItems.size() < MAX_REMINDERS) {
+ menu.setGroupVisible(MENU_GROUP_REMINDER, true);
+ menu.setGroupEnabled(MENU_GROUP_REMINDER, true);
+ } else {
+ menu.setGroupVisible(MENU_GROUP_REMINDER, false);
+ menu.setGroupEnabled(MENU_GROUP_REMINDER, false);
+ }
+
+ if (mExtraOptions.getVisibility() == View.VISIBLE) {
+ menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, false);
+ menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, true);
+ } else {
+ menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, true);
+ menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, false);
+ }
+
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case MENU_ADD_REMINDER:
+ // TODO: when adding a new reminder, make it different from the
+ // last one in the list (if any).
+ if (mDefaultReminderMinutes == 0) {
+ addReminder(this, this, mReminderItems, mReminderValues,
+ mReminderLabels, 10 /* minutes */);
+ } else {
+ addReminder(this, this, mReminderItems, mReminderValues,
+ mReminderLabels, mDefaultReminderMinutes);
+ }
+ updateRemindersVisibility();
+ return true;
+ case MENU_SHOW_EXTRA_OPTIONS:
+ mExtraOptions.setVisibility(View.VISIBLE);
+ return true;
+ case MENU_HIDE_EXTRA_OPTIONS:
+ mExtraOptions.setVisibility(View.GONE);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ // If we are creating a new event, do not create it if the
+ // title, location and description are all empty, in order to
+ // prevent accidental "no subject" event creations.
+ if (mUri != null || !isEmpty()) {
+ if (!save()) {
+ // We cannot exit this activity because the calendars
+ // are still loading.
+ return true;
+ }
+ }
+ break;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private void populateWhen() {
+ long startMillis = mStartTime.toMillis(false /* use isDst */);
+ long endMillis = mEndTime.toMillis(false /* use isDst */);
+ setDate(mStartDateButton, startMillis);
+ setDate(mEndDateButton, endMillis);
+
+ setTime(mStartTimeButton, startMillis);
+ setTime(mEndTimeButton, endMillis);
+
+ mStartDateButton.setOnClickListener(new DateClickListener(mStartTime));
+ mEndDateButton.setOnClickListener(new DateClickListener(mEndTime));
+
+ mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime));
+ mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime));
+ }
+
+ private void populateRepeats() {
+ Time time = mStartTime;
+ Resources r = getResources();
+ int resource = android.R.layout.simple_spinner_item;
+
+ String[] days = r.getStringArray(R.array.day_labels);
+ String[] ordinals = r.getStringArray(R.array.ordinal_labels);
+
+ // Only display "Custom" in the spinner if the device does not support the
+ // recurrence functionality of the event. Only display every weekday if
+ // the event starts on a weekday.
+ boolean isCustomRecurrence = isCustomRecurrence();
+ boolean isWeekdayEvent = isWeekdayEvent();
+
+ ArrayList<String> repeatArray = new ArrayList<String>(0);
+ ArrayList<Integer> recurrenceIndexes = new ArrayList<Integer>(0);
+
+ repeatArray.add(r.getString(R.string.does_not_repeat));
+ recurrenceIndexes.add(DOES_NOT_REPEAT);
+
+ repeatArray.add(r.getString(R.string.daily));
+ recurrenceIndexes.add(REPEATS_DAILY);
+
+ if (isWeekdayEvent) {
+ repeatArray.add(r.getString(R.string.every_weekday));
+ recurrenceIndexes.add(REPEATS_EVERY_WEEKDAY);
+ }
+
+ String format = r.getString(R.string.weekly);
+ repeatArray.add(String.format(format, time.format("%A")));
+ recurrenceIndexes.add(REPEATS_WEEKLY_ON_DAY);
+
+ // Calculate whether this is the 1st, 2nd, 3rd, 4th, or last appearance of the given day.
+ int dayNumber = (time.monthDay - 1) / 7;
+ format = r.getString(R.string.monthly_on_day_count);
+ repeatArray.add(String.format(format, ordinals[dayNumber], days[time.weekDay]));
+ recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY_COUNT);
+
+ format = r.getString(R.string.monthly_on_day);
+ repeatArray.add(String.format(format, time.monthDay));
+ recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY);
+
+ long when = time.toMillis(false);
+ format = r.getString(R.string.yearly);
+ int flags = 0;
+ if (DateFormat.is24HourFormat(this)) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ }
+ repeatArray.add(String.format(format, DateUtils.formatDateTime(this, when, flags)));
+ recurrenceIndexes.add(REPEATS_YEARLY);
+
+ if (isCustomRecurrence) {
+ repeatArray.add(r.getString(R.string.custom));
+ recurrenceIndexes.add(REPEATS_CUSTOM);
+ }
+ mRecurrenceIndexes = recurrenceIndexes;
+
+ int position = recurrenceIndexes.indexOf(DOES_NOT_REPEAT);
+ if (mRrule != null) {
+ if (isCustomRecurrence) {
+ position = recurrenceIndexes.indexOf(REPEATS_CUSTOM);
+ } else {
+ switch (mEventRecurrence.freq) {
+ case EventRecurrence.DAILY:
+ position = recurrenceIndexes.indexOf(REPEATS_DAILY);
+ break;
+ case EventRecurrence.WEEKLY:
+ if (mEventRecurrence.repeatsOnEveryWeekDay()) {
+ position = recurrenceIndexes.indexOf(REPEATS_EVERY_WEEKDAY);
+ } else {
+ position = recurrenceIndexes.indexOf(REPEATS_WEEKLY_ON_DAY);
+ }
+ break;
+ case EventRecurrence.MONTHLY:
+ if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
+ position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY_COUNT);
+ } else {
+ position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY);
+ }
+ break;
+ case EventRecurrence.YEARLY:
+ position = recurrenceIndexes.indexOf(REPEATS_YEARLY);
+ break;
+ }
+ }
+ }
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, resource, repeatArray);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mRepeatsSpinner.setAdapter(adapter);
+ mRepeatsSpinner.setSelection(position);
+ }
+
+ // Adds a reminder to the displayed list of reminders.
+ // Returns true if successfully added reminder, false if no reminders can
+ // be added.
+ static boolean addReminder(Activity activity, View.OnClickListener listener,
+ ArrayList<LinearLayout> items, ArrayList<Integer> values,
+ ArrayList<String> labels, int minutes) {
+
+ if (items.size() >= MAX_REMINDERS) {
+ return false;
+ }
+
+ LayoutInflater inflater = activity.getLayoutInflater();
+ LinearLayout parent = (LinearLayout) activity.findViewById(R.id.reminder_items_container);
+ LinearLayout reminderItem = (LinearLayout) inflater.inflate(R.layout.edit_reminder_item, null);
+ parent.addView(reminderItem);
+
+ Spinner spinner = (Spinner) reminderItem.findViewById(R.id.reminder_value);
+ Resources res = activity.getResources();
+ spinner.setPrompt(res.getString(R.string.reminders_label));
+ int resource = android.R.layout.simple_spinner_item;
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(activity, resource, labels);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinner.setAdapter(adapter);
+
+ ImageButton reminderRemoveButton;
+ reminderRemoveButton = (ImageButton) reminderItem.findViewById(R.id.reminder_remove);
+ reminderRemoveButton.setOnClickListener(listener);
+
+ int index = findMinutesInReminderList(values, minutes);
+ spinner.setSelection(index);
+ items.add(reminderItem);
+
+ return true;
+ }
+
+ static void addMinutesToList(Context context, ArrayList<Integer> values,
+ ArrayList<String> labels, int minutes) {
+ int index = values.indexOf(minutes);
+ if (index != -1) {
+ return;
+ }
+
+ // The requested "minutes" does not exist in the list, so insert it
+ // into the list.
+
+ String label = constructReminderLabel(context, minutes, false);
+ int len = values.size();
+ for (int i = 0; i < len; i++) {
+ if (minutes < values.get(i)) {
+ values.add(i, minutes);
+ labels.add(i, label);
+ return;
+ }
+ }
+
+ values.add(minutes);
+ labels.add(len, label);
+ }
+
+ /**
+ * Finds the index of the given "minutes" in the "values" list.
+ *
+ * @param values the list of minutes corresponding to the spinner choices
+ * @param minutes the minutes to search for in the values list
+ * @return the index of "minutes" in the "values" list
+ */
+ private static int findMinutesInReminderList(ArrayList<Integer> values, int minutes) {
+ int index = values.indexOf(minutes);
+ if (index == -1) {
+ // This should never happen.
+ Log.e("Cal", "Cannot find minutes (" + minutes + ") in list");
+ return 0;
+ }
+ return index;
+ }
+
+ // Constructs a label given an arbitrary number of minutes. For example,
+ // if the given minutes is 63, then this returns the string "63 minutes".
+ // As another example, if the given minutes is 120, then this returns
+ // "2 hours".
+ static String constructReminderLabel(Context context, int minutes, boolean abbrev) {
+ Resources resources = context.getResources();
+ int value, resId;
+
+ if (minutes % 60 != 0) {
+ value = minutes;
+ if (abbrev) {
+ resId = R.plurals.Nmins;
+ } else {
+ resId = R.plurals.Nminutes;
+ }
+ } else if (minutes % (24 * 60) != 0) {
+ value = minutes / 60;
+ resId = R.plurals.Nhours;
+ } else {
+ value = minutes / ( 24 * 60);
+ resId = R.plurals.Ndays;
+ }
+
+ String format = resources.getQuantityString(resId, value);
+ return String.format(format, value);
+ }
+
+ private void updateRemindersVisibility() {
+ if (mReminderItems.size() == 0) {
+ mRemindersSeparator.setVisibility(View.GONE);
+ mRemindersContainer.setVisibility(View.GONE);
+ } else {
+ mRemindersSeparator.setVisibility(View.VISIBLE);
+ mRemindersContainer.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void setDate(TextView view, long millis) {
+ int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
+ DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH |
+ DateUtils.FORMAT_ABBREV_WEEKDAY;
+ view.setText(DateUtils.formatDateTime(this, millis, flags));
+ }
+
+ private void setTime(TextView view, long millis) {
+ int flags = DateUtils.FORMAT_SHOW_TIME;
+ if (DateFormat.is24HourFormat(this)) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ }
+ view.setText(DateUtils.formatDateTime(this, millis, flags));
+ }
+
+ // Saves the event. Returns true if it is okay to exit this activity.
+ private boolean save() {
+ // If we are creating a new event, then make sure we wait until the
+ // query to fetch the list of calendars has finished.
+ if (mEventCursor == null) {
+ if (!mCalendarsQueryComplete) {
+ // Wait for the calendars query to finish.
+ if (mLoadingCalendarsDialog == null) {
+ // Create the progress dialog
+ mLoadingCalendarsDialog = ProgressDialog.show(this,
+ getText(R.string.loading_calendars_title),
+ getText(R.string.loading_calendars_message),
+ true, true, this);
+ mSaveAfterQueryComplete = true;
+ }
+ return false;
+ }
+
+ // Avoid creating a new event if the calendars cursor is empty. This
+ // shouldn't ever happen since the setup wizard should ensure the user
+ // has a calendar.
+ if (mCalendarsCursor == null || mCalendarsCursor.getCount() == 0) {
+ Log.w("Cal", "The calendars table does not contain any calendars."
+ + " New event was not created.");
+ return true;
+ }
+ Toast.makeText(this, R.string.creating_event, Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show();
+ }
+
+ ContentResolver cr = getContentResolver();
+ ContentValues values = getContentValuesFromUi();
+ Uri uri = mUri;
+
+ // For recurring events, we must make sure that we use duration rather
+ // than dtend.
+ if (uri == null) {
+ // Create new event with new contents
+ addRecurrenceRule(values);
+ uri = cr.insert(Events.CONTENT_URI, values);
+
+ } else if (mRrule == null) {
+ // Modify contents of a non-repeating event
+ addRecurrenceRule(values);
+ checkTimeDependentFields(values);
+ cr.update(uri, values, null, null);
+
+ } else if (mInitialValues.getAsString(Events.RRULE) == null) {
+ // This event was changed from a non-repeating event to a
+ // repeating event.
+ addRecurrenceRule(values);
+ values.remove(Events.DTEND);
+ cr.update(uri, values, null, null);
+
+ } else if (mModification == MODIFY_SELECTED) {
+ // Modify contents of the current instance of repeating event
+
+ // Create a recurrence exception
+ long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
+ values.put(Events.ORIGINAL_EVENT, mEventCursor.getString(EVENT_INDEX_SYNC_ID));
+ values.put(Events.ORIGINAL_INSTANCE_TIME, begin);
+ boolean allDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
+ values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
+
+ uri = cr.insert(Events.CONTENT_URI, values);
+
+ } else if (mModification == MODIFY_ALL_FOLLOWING) {
+ // Modify this instance and all future instances of repeating event
+ addRecurrenceRule(values);
+
+ if (mRrule == null) {
+ // We've changed a recurring event to a non-recurring event.
+ // If the event we are editing is the first in the series,
+ // then delete the whole series. Otherwise, update the series
+ // to end at the new start time.
+ if (isFirstEventInSeries()) {
+ cr.delete(uri, null, null);
+ } else {
+ // Update the current repeating event to end at the new
+ // start time.
+ updatePastEvents(cr, uri);
+ }
+ uri = cr.insert(Events.CONTENT_URI, values);
+ } else {
+ if (isFirstEventInSeries()) {
+ checkTimeDependentFields(values);
+ values.remove(Events.DTEND);
+ cr.update(uri, values, null, null);
+ } else {
+ // Update the current repeating event to end at the new
+ // start time.
+ updatePastEvents(cr, uri);
+
+ // Create a new event with the user-modified fields
+ values.remove(Events.DTEND);
+ uri = cr.insert(Events.CONTENT_URI, values);
+ }
+ }
+
+ } else if (mModification == MODIFY_ALL) {
+
+ // Modify all instances of repeating event
+ addRecurrenceRule(values);
+
+ if (mRrule == null) {
+ // We've changed a recurring event to a non-recurring event.
+ // Delete the whole series and replace it with a new
+ // non-recurring event.
+ cr.delete(uri, null, null);
+ uri = cr.insert(Events.CONTENT_URI, values);
+ } else {
+ checkTimeDependentFields(values);
+ values.remove(Events.DTEND);
+ cr.update(uri, values, null, null);
+ }
+ }
+
+ if (uri != null) {
+ long eventId = ContentUris.parseId(uri);
+ ArrayList<Integer> reminderMinutes = reminderItemsToMinutes(mReminderItems,
+ mReminderValues);
+ saveReminders(cr, eventId, reminderMinutes, mOriginalMinutes);
+ }
+ return true;
+ }
+
+ private boolean isFirstEventInSeries() {
+ int dtStart = mEventCursor.getColumnIndexOrThrow(Events.DTSTART);
+ long start = mEventCursor.getLong(dtStart);
+ return start == mStartTime.toMillis(true);
+ }
+
+ private void updatePastEvents(ContentResolver cr, Uri uri) {
+ long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
+ String oldDuration = mEventCursor.getString(EVENT_INDEX_DURATION);
+ boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
+ String oldRrule = mEventCursor.getString(EVENT_INDEX_RRULE);
+ mEventRecurrence.parse(oldRrule);
+
+ Time untilTime = new Time();
+ long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
+ ContentValues oldValues = new ContentValues();
+
+ // The "until" time must be in UTC time in order for Google calendar
+ // to display it properly. For all-day events, the "until" time string
+ // must include just the date field, and not the time field. The
+ // repeating events repeat up to and including the "until" time.
+ untilTime.timezone = Time.TIMEZONE_UTC;
+
+ // Subtract one second from the old begin time to get the new
+ // "until" time.
+ untilTime.set(begin - 1000); // subtract one second (1000 millis)
+ if (allDay) {
+ untilTime.hour = 0;
+ untilTime.minute = 0;
+ untilTime.second = 0;
+ untilTime.allDay = true;
+ untilTime.normalize(false);
+
+ // For all-day events, the duration must be in days, not seconds.
+ // Otherwise, Google Calendar will (mistakenly) change this event
+ // into a non-all-day event.
+ int len = oldDuration.length();
+ if (oldDuration.charAt(0) == 'P' && oldDuration.charAt(len - 1) == 'S') {
+ int seconds = Integer.parseInt(oldDuration.substring(1, len - 1));
+ int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
+ oldDuration = "P" + days + "D";
+ }
+ }
+ mEventRecurrence.until = untilTime.format2445();
+
+ oldValues.put(Events.DTSTART, oldStartMillis);
+ oldValues.put(Events.DURATION, oldDuration);
+ oldValues.put(Events.RRULE, mEventRecurrence.toString());
+ cr.update(uri, oldValues, null, null);
+ }
+
+ private void checkTimeDependentFields(ContentValues values) {
+ long oldBegin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
+ long oldEnd = mInitialValues.getAsLong(EVENT_END_TIME);
+ boolean oldAllDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
+ String oldRrule = mInitialValues.getAsString(Events.RRULE);
+ String oldTimezone = mInitialValues.getAsString(Events.EVENT_TIMEZONE);
+
+ long newBegin = values.getAsLong(Events.DTSTART);
+ long newEnd = values.getAsLong(Events.DTEND);
+ boolean newAllDay = values.getAsInteger(Events.ALL_DAY) != 0;
+ String newRrule = values.getAsString(Events.RRULE);
+ String newTimezone = values.getAsString(Events.EVENT_TIMEZONE);
+
+ // If none of the time-dependent fields changed, then remove them.
+ if (oldBegin == newBegin && oldEnd == newEnd && oldAllDay == newAllDay
+ && TextUtils.equals(oldRrule, newRrule)
+ && TextUtils.equals(oldTimezone, newTimezone)) {
+ values.remove(Events.DTSTART);
+ values.remove(Events.DTEND);
+ values.remove(Events.DURATION);
+ values.remove(Events.ALL_DAY);
+ values.remove(Events.RRULE);
+ values.remove(Events.EVENT_TIMEZONE);
+ return;
+ }
+
+ if (oldRrule == null || newRrule == null) {
+ return;
+ }
+
+ // If we are modifying all events then we need to set DTSTART to the
+ // start time of the first event in the series, not the current
+ // date and time. If the start time of the event was changed
+ // (from, say, 3pm to 4pm), then we want to add the time difference
+ // to the start time of the first event in the series (the DTSTART
+ // value). If we are modifying one instance or all following instances,
+ // then we leave the DTSTART field alone.
+ if (mModification == MODIFY_ALL) {
+ long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
+ if (oldBegin != newBegin) {
+ // The user changed the start time of this event
+ long offset = newBegin - oldBegin;
+ oldStartMillis += offset;
+ }
+ values.put(Events.DTSTART, oldStartMillis);
+ }
+ }
+
+ static ArrayList<Integer> reminderItemsToMinutes(ArrayList<LinearLayout> reminderItems,
+ ArrayList<Integer> reminderValues) {
+ int len = reminderItems.size();
+ ArrayList<Integer> reminderMinutes = new ArrayList<Integer>(len);
+ for (int index = 0; index < len; index++) {
+ LinearLayout layout = reminderItems.get(index);
+ Spinner spinner = (Spinner) layout.findViewById(R.id.reminder_value);
+ int minutes = reminderValues.get(spinner.getSelectedItemPosition());
+ reminderMinutes.add(minutes);
+ }
+ return reminderMinutes;
+ }
+
+ /**
+ * Saves the reminders, if they changed. Returns true if the database
+ * was updated.
+ *
+ * @param cr the ContentResolver
+ * @param eventId the id of the event whose reminders are being updated
+ * @param reminderMinutes the array of reminders set by the user
+ * @param originalMinutes the original array of reminders
+ * @return true if the database was updated
+ */
+ static boolean saveReminders(ContentResolver cr, long eventId,
+ ArrayList<Integer> reminderMinutes, ArrayList<Integer> originalMinutes) {
+ // If the reminders have not changed, then don't update the database
+ if (reminderMinutes.equals(originalMinutes)) {
+ return false;
+ }
+
+ // Delete all the existing reminders for this event
+ String where = Reminders.EVENT_ID + "=?";
+ String[] args = new String[] { Long.toString(eventId) };
+ cr.delete(Reminders.CONTENT_URI, where, args);
+
+ // Update the "hasAlarm" field for the event
+ ContentValues values = new ContentValues();
+ int len = reminderMinutes.size();
+ values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0);
+ Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
+ cr.update(uri, values, null /* where */, null /* selection args */);
+
+ // Insert the new reminders, if any
+ for (int i = 0; i < len; i++) {
+ int minutes = reminderMinutes.get(i);
+
+ values.clear();
+ values.put(Reminders.MINUTES, minutes);
+ values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
+ values.put(Reminders.EVENT_ID, eventId);
+ cr.insert(Reminders.CONTENT_URI, values);
+ }
+ return true;
+ }
+
+ private void addRecurrenceRule(ContentValues values) {
+ updateRecurrenceRule();
+
+ if (mRrule == null) {
+ return;
+ }
+
+ values.put(Events.RRULE, mRrule);
+ long end = mEndTime.toMillis(true /* ignore dst */);
+ long start = mStartTime.toMillis(true /* ignore dst */);
+ String duration;
+
+ boolean isAllDay = mAllDayCheckBox.isChecked();
+ if (isAllDay) {
+ long days = (end - start + DateUtils.DAY_IN_MILLIS - 1) / DateUtils.DAY_IN_MILLIS;
+ duration = "P" + days + "D";
+ } else {
+ long seconds = (end - start) / DateUtils.SECOND_IN_MILLIS;
+ duration = "P" + seconds + "S";
+ }
+ values.put(Events.DURATION, duration);
+ }
+
+ private void updateRecurrenceRule() {
+ int position = mRepeatsSpinner.getSelectedItemPosition();
+ int selection = mRecurrenceIndexes.get(position);
+
+ if (selection == DOES_NOT_REPEAT) {
+ mRrule = null;
+ return;
+ } else if (selection == REPEATS_CUSTOM) {
+ // Keep custom recurrence as before.
+ return;
+ } else if (selection == REPEATS_DAILY) {
+ mEventRecurrence.freq = EventRecurrence.DAILY;
+ } else if (selection == REPEATS_EVERY_WEEKDAY) {
+ mEventRecurrence.freq = EventRecurrence.WEEKLY;
+ int dayCount = 5;
+ int[] byday = new int[dayCount];
+ int[] bydayNum = new int[dayCount];
+
+ byday[0] = EventRecurrence.MO;
+ byday[1] = EventRecurrence.TU;
+ byday[2] = EventRecurrence.WE;
+ byday[3] = EventRecurrence.TH;
+ byday[4] = EventRecurrence.FR;
+ for (int day = 0; day < dayCount; day++) {
+ bydayNum[day] = 0;
+ }
+
+ mEventRecurrence.byday = byday;
+ mEventRecurrence.bydayNum = bydayNum;
+ mEventRecurrence.bydayCount = dayCount;
+ } else if (selection == REPEATS_WEEKLY_ON_DAY) {
+ mEventRecurrence.freq = EventRecurrence.WEEKLY;
+ int[] days = new int[1];
+ int dayCount = 1;
+ int[] dayNum = new int[dayCount];
+
+ days[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
+ // not sure why this needs to be zero, but set it for now.
+ dayNum[0] = 0;
+
+ mEventRecurrence.byday = days;
+ mEventRecurrence.bydayNum = dayNum;
+ mEventRecurrence.bydayCount = dayCount;
+ } else if (selection == REPEATS_MONTHLY_ON_DAY) {
+ mEventRecurrence.freq = EventRecurrence.MONTHLY;
+ mEventRecurrence.bydayCount = 0;
+ mEventRecurrence.bymonthdayCount = 1;
+ int[] bymonthday = new int[1];
+ bymonthday[0] = mStartTime.monthDay;
+ mEventRecurrence.bymonthday = bymonthday;
+ } else if (selection == REPEATS_MONTHLY_ON_DAY_COUNT) {
+ mEventRecurrence.freq = EventRecurrence.MONTHLY;
+ mEventRecurrence.bydayCount = 1;
+ mEventRecurrence.bymonthdayCount = 0;
+
+ int[] byday = new int[1];
+ int[] bydayNum = new int[1];
+ // Compute the week number (for example, the "2nd" Monday)
+ int dayCount = 1 + ((mStartTime.monthDay - 1) / 7);
+ if (dayCount == 5) {
+ dayCount = -1;
+ }
+ bydayNum[0] = dayCount;
+ byday[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
+ mEventRecurrence.byday = byday;
+ mEventRecurrence.bydayNum = bydayNum;
+ } else if (selection == REPEATS_YEARLY) {
+ mEventRecurrence.freq = EventRecurrence.YEARLY;
+ }
+
+ // Set the week start day.
+ mEventRecurrence.wkst = EventRecurrence.calendarDay2Day(mFirstDayOfWeek);
+ mRrule = mEventRecurrence.toString();
+ }
+
+ private ContentValues getContentValuesFromUi() {
+ String title = mTitleTextView.getText().toString();
+ boolean isAllDay = mAllDayCheckBox.isChecked();
+ String location = mLocationTextView.getText().toString();
+ String description = mDescriptionTextView.getText().toString();
+
+ ContentValues values = new ContentValues();
+
+ String timezone = null;
+ long startMillis;
+ long endMillis;
+ long calendarId;
+ if (isAllDay) {
+ // Reset start and end time, increment the monthDay by 1, and set
+ // the timezone to UTC, as required for all-day events.
+ timezone = Time.TIMEZONE_UTC;
+ mStartTime.hour = 0;
+ mStartTime.minute = 0;
+ mStartTime.second = 0;
+ mStartTime.timezone = timezone;
+ startMillis = mStartTime.normalize(true);
+
+ mEndTime.hour = 0;
+ mEndTime.minute = 0;
+ mEndTime.second = 0;
+ mEndTime.monthDay++;
+ mEndTime.timezone = timezone;
+ endMillis = mEndTime.normalize(true);
+
+ if (mEventCursor == null) {
+ // This is a new event
+ calendarId = mCalendarsSpinner.getSelectedItemId();
+ } else {
+ calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
+ }
+ } else {
+ startMillis = mStartTime.toMillis(true);
+ endMillis = mEndTime.toMillis(true);
+ if (mEventCursor != null) {
+ // This is an existing event
+ timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
+
+ // The timezone might be null if we are changing an existing
+ // all-day event to a non-all-day event. We need to assign
+ // a timezone to the non-all-day event.
+ if (TextUtils.isEmpty(timezone)) {
+ timezone = TimeZone.getDefault().getID();
+ }
+ calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
+ } else {
+ // This is a new event
+ calendarId = mCalendarsSpinner.getSelectedItemId();
+
+ // The timezone for a new event is the currently displayed
+ // timezone, NOT the timezone of the containing calendar.
+ timezone = TimeZone.getDefault().getID();
+ }
+ }
+
+ values.put(Events.CALENDAR_ID, calendarId);
+ values.put(Events.EVENT_TIMEZONE, timezone);
+ values.put(Events.TITLE, title);
+ values.put(Events.ALL_DAY, isAllDay ? 1 : 0);
+ values.put(Events.DTSTART, startMillis);
+ values.put(Events.DTEND, endMillis);
+ values.put(Events.DESCRIPTION, description);
+ values.put(Events.EVENT_LOCATION, location);
+ values.put(Events.TRANSPARENCY, mAvailabilitySpinner.getSelectedItemPosition());
+
+ int visibility = mVisibilitySpinner.getSelectedItemPosition();
+ if (visibility > 0) {
+ // For now we the array contains the values 0, 2, and 3. We add one to match.
+ visibility++;
+ }
+ values.put(Events.VISIBILITY, visibility);
+
+ return values;
+ }
+
+ private boolean isEmpty() {
+ String title = mTitleTextView.getText().toString();
+ if (title.length() > 0) {
+ return false;
+ }
+
+ String location = mLocationTextView.getText().toString();
+ if (location.length() > 0) {
+ return false;
+ }
+
+ String description = mDescriptionTextView.getText().toString();
+ if (description.length() > 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean isCustomRecurrence() {
+
+ if (mEventRecurrence.until != null || mEventRecurrence.interval != 0) {
+ return true;
+ }
+
+ if (mEventRecurrence.freq == 0) {
+ return false;
+ }
+
+ switch (mEventRecurrence.freq) {
+ case EventRecurrence.DAILY:
+ return false;
+ case EventRecurrence.WEEKLY:
+ if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) {
+ return false;
+ } else if (mEventRecurrence.bydayCount == 1) {
+ return false;
+ }
+ break;
+ case EventRecurrence.MONTHLY:
+ if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
+ return false;
+ } else if (mEventRecurrence.bydayCount == 0 && mEventRecurrence.bymonthdayCount == 1) {
+ return false;
+ }
+ break;
+ case EventRecurrence.YEARLY:
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean isWeekdayEvent() {
+ if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/calendar/EditResponseHelper.java b/src/com/android/calendar/EditResponseHelper.java
new file mode 100644
index 00000000..378ef458
--- /dev/null
+++ b/src/com/android/calendar/EditResponseHelper.java
@@ -0,0 +1,92 @@
+/*
+ * 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.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.widget.Button;
+
+/**
+ * A helper class for editing the response to an invitation when the invitation
+ * is a repeating event.
+ */
+public class EditResponseHelper implements DialogInterface.OnClickListener {
+ private final Activity mParent;
+ private int mWhichEvents = -1;
+ private AlertDialog mAlertDialog;
+
+ /**
+ * This callback is passed in to this object when this object is created
+ * and is invoked when the "Ok" button is selected.
+ */
+ private DialogInterface.OnClickListener mDialogListener;
+
+ public EditResponseHelper(Activity parent) {
+ mParent = parent;
+ }
+
+ public void setOnClickListener(DialogInterface.OnClickListener listener) {
+ mDialogListener = listener;
+ }
+
+ public int getWhichEvents() {
+ return mWhichEvents;
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ }
+
+ /**
+ * This callback is used when a list item is selected
+ */
+ private DialogInterface.OnClickListener mListListener =
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mWhichEvents = which;
+
+ // Enable the "ok" button now that the user has selected which
+ // events in the series to delete.
+ Button ok = mAlertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ ok.setEnabled(true);
+ }
+ };
+
+ public void showDialog(int whichEvents) {
+ // We need to have a non-null listener, otherwise we get null when
+ // we try to fetch the "Ok" button.
+ if (mDialogListener == null) {
+ mDialogListener = this;
+ }
+ AlertDialog dialog = new AlertDialog.Builder(mParent)
+ .setTitle(R.string.change_response_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setSingleChoiceItems(R.array.change_response_labels, whichEvents,
+ mListListener)
+ .setPositiveButton(android.R.string.ok, mDialogListener)
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ mAlertDialog = dialog;
+
+ if (whichEvents == -1) {
+ // Disable the "Ok" button until the user selects which events to
+ // delete.
+ Button ok = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ ok.setEnabled(false);
+ }
+ }
+}
diff --git a/src/com/android/calendar/Event.java b/src/com/android/calendar/Event.java
new file mode 100644
index 00000000..c15e5b54
--- /dev/null
+++ b/src/com/android/calendar/Event.java
@@ -0,0 +1,625 @@
+/*
+ * Copyright (C) 2007 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.Context;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.os.Debug;
+import android.preference.PreferenceManager;
+import android.provider.Calendar.Attendees;
+import android.provider.Calendar.Instances;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.concurrent.atomic.AtomicInteger;
+
+// TODO: should Event be Parcelable so it can be passed via Intents?
+public class Event implements Comparable, Cloneable {
+
+ private static final boolean PROFILE = false;
+
+ private static final String[] PROJECTION = new String[] {
+ Instances.TITLE, // 0
+ Instances.EVENT_LOCATION, // 1
+ Instances.ALL_DAY, // 2
+ Instances.COLOR, // 3
+ Instances.EVENT_TIMEZONE, // 4
+ Instances.EVENT_ID, // 5
+ Instances.BEGIN, // 6
+ Instances.END, // 7
+ Instances._ID, // 8
+ Instances.START_DAY, // 9
+ Instances.END_DAY, // 10
+ Instances.START_MINUTE, // 11
+ Instances.END_MINUTE, // 12
+ Instances.HAS_ALARM, // 13
+ Instances.RRULE, // 14
+ Instances.RDATE, // 15
+ Instances.SELF_ATTENDEE_STATUS, // 16
+ };
+
+ // The indices for the projection array above.
+ private static final int PROJECTION_TITLE_INDEX = 0;
+ private static final int PROJECTION_LOCATION_INDEX = 1;
+ private static final int PROJECTION_ALL_DAY_INDEX = 2;
+ private static final int PROJECTION_COLOR_INDEX = 3;
+ private static final int PROJECTION_TIMEZONE_INDEX = 4;
+ private static final int PROJECTION_EVENT_ID_INDEX = 5;
+ private static final int PROJECTION_BEGIN_INDEX = 6;
+ private static final int PROJECTION_END_INDEX = 7;
+ private static final int PROJECTION_START_DAY_INDEX = 9;
+ private static final int PROJECTION_END_DAY_INDEX = 10;
+ private static final int PROJECTION_START_MINUTE_INDEX = 11;
+ private static final int PROJECTION_END_MINUTE_INDEX = 12;
+ private static final int PROJECTION_HAS_ALARM_INDEX = 13;
+ private static final int PROJECTION_RRULE_INDEX = 14;
+ private static final int PROJECTION_RDATE_INDEX = 15;
+ private static final int PROJECTION_SELF_ATTENDEE_STATUS_INDEX = 16;
+
+ public long id;
+ public int color;
+ public CharSequence title;
+ public CharSequence location;
+ public boolean allDay;
+
+ public int startDay; // start Julian day
+ public int endDay; // end Julian day
+ public int startTime; // Start and end time are in minutes since midnight
+ public int endTime;
+
+ public long startMillis; // UTC milliseconds since the epoch
+ public long endMillis; // UTC milliseconds since the epoch
+ private int mColumn;
+ private int mMaxColumns;
+
+ public boolean hasAlarm;
+ public boolean isRepeating;
+
+ public int selfAttendeeStatus;
+
+ // The coordinates of the event rectangle drawn on the screen.
+ public float left;
+ public float right;
+ public float top;
+ public float bottom;
+
+ // These 4 fields are used for navigating among events within the selected
+ // hour in the Day and Week view.
+ public Event nextRight;
+ public Event nextLeft;
+ public Event nextUp;
+ public Event nextDown;
+
+ private static final int MIDNIGHT_IN_MINUTES = 24 * 60;
+
+ @Override
+ public final Object clone() {
+ Event e = new Event();
+
+ e.title = title;
+ e.color = color;
+ e.location = location;
+ e.allDay = allDay;
+ e.startDay = startDay;
+ e.endDay = endDay;
+ e.startTime = startTime;
+ e.endTime = endTime;
+ e.startMillis = startMillis;
+ e.endMillis = endMillis;
+ e.hasAlarm = hasAlarm;
+ e.isRepeating = isRepeating;
+ e.selfAttendeeStatus = selfAttendeeStatus;
+
+ return e;
+ }
+
+ public final void copyTo(Event dest) {
+ dest.id = id;
+ dest.title = title;
+ dest.color = color;
+ dest.location = location;
+ dest.allDay = allDay;
+ dest.startDay = startDay;
+ dest.endDay = endDay;
+ dest.startTime = startTime;
+ dest.endTime = endTime;
+ dest.startMillis = startMillis;
+ dest.endMillis = endMillis;
+ dest.hasAlarm = hasAlarm;
+ dest.isRepeating = isRepeating;
+ dest.selfAttendeeStatus = selfAttendeeStatus;
+ }
+
+ public static final Event newInstance() {
+ Event e = new Event();
+
+ e.id = 0;
+ e.title = null;
+ e.color = 0;
+ e.location = null;
+ e.allDay = false;
+ e.startDay = 0;
+ e.endDay = 0;
+ e.startTime = 0;
+ e.endTime = 0;
+ e.startMillis = 0;
+ e.endMillis = 0;
+ e.hasAlarm = false;
+ e.isRepeating = false;
+ e.selfAttendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
+
+ return e;
+ }
+
+ /**
+ * Compares this event to the given event. This is just used for checking
+ * if two events differ. It's not used for sorting anymore.
+ */
+ public final int compareTo(Object obj) {
+ Event e = (Event) obj;
+
+ // The earlier start day and time comes first
+ if (startDay < e.startDay) return -1;
+ if (startDay > e.startDay) return 1;
+ if (startTime < e.startTime) return -1;
+ if (startTime > e.startTime) return 1;
+
+ // The later end time comes first (in order to put long strips on
+ // the left).
+ if (endDay < e.endDay) return 1;
+ if (endDay > e.endDay) return -1;
+ if (endTime < e.endTime) return 1;
+ if (endTime > e.endTime) return -1;
+
+ // Sort all-day events before normal events.
+ if (allDay && !e.allDay) return -1;
+ if (!allDay && e.allDay) return 1;
+
+ // If two events have the same time range, then sort them in
+ // alphabetical order based on their titles.
+ int cmp = compareStrings(title, e.title);
+ if (cmp != 0) {
+ return cmp;
+ }
+
+ // If the titles are the same then compare the other fields
+ // so that we can use this function to check for differences
+ // between events.
+ cmp = compareStrings(location, e.location);
+ if (cmp != 0) {
+ return cmp;
+ }
+ return 0;
+ }
+
+ /**
+ * Compare string a with string b, but if either string is null,
+ * then treat it (the null) as if it were the empty string ("").
+ *
+ * @param a the first string
+ * @param b the second string
+ * @return the result of comparing a with b after replacing null
+ * strings with "".
+ */
+ private int compareStrings(CharSequence a, CharSequence b) {
+ String aStr, bStr;
+ if (a != null) {
+ aStr = a.toString();
+ } else {
+ aStr = "";
+ }
+ if (b != null) {
+ bStr = b.toString();
+ } else {
+ bStr = "";
+ }
+ return aStr.compareTo(bStr);
+ }
+
+ /**
+ * Loads <i>days</i> days worth of instances starting at <i>start</i>.
+ */
+ public static void loadEvents(Context context, ArrayList<Event> events,
+ long start, int days, int requestId, AtomicInteger sequenceNumber) {
+
+ if (PROFILE) {
+ Debug.startMethodTracing("loadEvents");
+ }
+
+ Cursor c = null;
+
+ events.clear();
+ try {
+ Time local = new Time();
+ int count;
+
+ local.set(start);
+ int startDay = Time.getJulianDay(start, local.gmtoff);
+ int endDay = startDay + days;
+
+ local.monthDay += days;
+ long end = local.normalize(true /* ignore isDst */);
+
+ // Widen the time range that we query by one day on each end
+ // so that we can catch all-day events. All-day events are
+ // stored starting at midnight in UTC but should be included
+ // in the list of events starting at midnight local time.
+ // This may fetch more events than we actually want, so we
+ // filter them out below.
+ //
+ // The sort order is: events with an earlier start time occur
+ // first and if the start times are the same, then events with
+ // a later end time occur first. The later end time is ordered
+ // first so that long rectangles in the calendar views appear on
+ // the left side. If the start and end times of two events are
+ // the same then we sort alphabetically on the title. This isn't
+ // required for correctness, it just adds a nice touch.
+
+ String orderBy = Instances.SORT_CALENDAR_VIEW;
+
+ // Respect the preference to show/hide declined events
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean hideDeclined = prefs.getBoolean(CalendarPreferenceActivity.KEY_HIDE_DECLINED,
+ false);
+
+ String where = null;
+ if (hideDeclined) {
+ where = Instances.SELF_ATTENDEE_STATUS + "!=" + Attendees.ATTENDEE_STATUS_DECLINED;
+ }
+
+ c = Instances.query(context.getContentResolver(), PROJECTION,
+ start - DateUtils.DAY_IN_MILLIS, end + DateUtils.DAY_IN_MILLIS, where, orderBy);
+
+ if (c == null) {
+ Log.e("Cal", "loadEvents() returned null cursor!");
+ return;
+ }
+
+ // Check if we should return early because there are more recent
+ // load requests waiting.
+ if (requestId != sequenceNumber.get()) {
+ return;
+ }
+
+ count = c.getCount();
+
+ if (count == 0) {
+ return;
+ }
+
+ Resources res = context.getResources();
+ while (c.moveToNext()) {
+ Event e = new Event();
+
+ e.id = c.getLong(PROJECTION_EVENT_ID_INDEX);
+ e.title = c.getString(PROJECTION_TITLE_INDEX);
+ e.location = c.getString(PROJECTION_LOCATION_INDEX);
+ e.allDay = c.getInt(PROJECTION_ALL_DAY_INDEX) != 0;
+ String timezone = c.getString(PROJECTION_TIMEZONE_INDEX);
+
+ if (e.title == null || e.title.length() == 0) {
+ e.title = res.getString(R.string.no_title_label);
+ }
+
+ if (!c.isNull(PROJECTION_COLOR_INDEX)) {
+ // Read the color from the database
+ e.color = c.getInt(PROJECTION_COLOR_INDEX);
+ } else {
+ e.color = res.getColor(R.color.event_center);
+ }
+
+ long eStart = c.getLong(PROJECTION_BEGIN_INDEX);
+ long eEnd = c.getLong(PROJECTION_END_INDEX);
+
+ e.startMillis = eStart;
+ e.startTime = c.getInt(PROJECTION_START_MINUTE_INDEX);
+ e.startDay = c.getInt(PROJECTION_START_DAY_INDEX);
+
+ e.endMillis = eEnd;
+ e.endTime = c.getInt(PROJECTION_END_MINUTE_INDEX);
+ e.endDay = c.getInt(PROJECTION_END_DAY_INDEX);
+
+ if (e.startDay > endDay || e.endDay < startDay) {
+ continue;
+ }
+
+ e.hasAlarm = c.getInt(PROJECTION_HAS_ALARM_INDEX) != 0;
+
+ // Check if this is a repeating event
+ String rrule = c.getString(PROJECTION_RRULE_INDEX);
+ String rdate = c.getString(PROJECTION_RDATE_INDEX);
+ if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate)) {
+ e.isRepeating = true;
+ } else {
+ e.isRepeating = false;
+ }
+
+ e.selfAttendeeStatus = c.getInt(PROJECTION_SELF_ATTENDEE_STATUS_INDEX);
+
+ events.add(e);
+ }
+
+ computePositions(events);
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ if (PROFILE) {
+ Debug.stopMethodTracing();
+ }
+ }
+ }
+
+ /**
+ * Computes a position for each event. Each event is displayed
+ * as a non-overlapping rectangle. For normal events, these rectangles
+ * are displayed in separate columns in the week view and day view. For
+ * all-day events, these rectangles are displayed in separate rows along
+ * the top. In both cases, each event is assigned two numbers: N, and
+ * Max, that specify that this event is the Nth event of Max number of
+ * events that are displayed in a group. The width and position of each
+ * rectangle depend on the maximum number of rectangles that occur at
+ * the same time.
+ *
+ * @param eventsList the list of events, sorted into increasing time order
+ */
+ static void computePositions(ArrayList<Event> eventsList) {
+ if (eventsList == null)
+ return;
+
+ // Compute the column positions separately for the all-day events
+ doComputePositions(eventsList, false);
+ doComputePositions(eventsList, true);
+ }
+
+ private static void doComputePositions(ArrayList<Event> eventsList,
+ boolean doAllDayEvents) {
+ ArrayList<Event> activeList = new ArrayList<Event>();
+ ArrayList<Event> groupList = new ArrayList<Event>();
+
+ long colMask = 0;
+ int maxCols = 0;
+ for (Event event : eventsList) {
+ // Process all-day events separately
+ if (event.allDay != doAllDayEvents)
+ continue;
+
+ long start = event.getStartMillis();
+ if (false && event.allDay) {
+ Event e = event;
+ Log.i("Cal", "event start,end day: " + e.startDay + "," + e.endDay
+ + " start,end time: " + e.startTime + "," + e.endTime
+ + " start,end millis: " + e.getStartMillis() + "," + e.getEndMillis()
+ + " " + e.title);
+ }
+
+ // Remove the inactive events. An event on the active list
+ // becomes inactive when its end time is less than or equal to
+ // the current event's start time.
+ Iterator<Event> iter = activeList.iterator();
+ while (iter.hasNext()) {
+ Event active = iter.next();
+ if (active.getEndMillis() <= start) {
+ if (false && event.allDay) {
+ Event e = active;
+ Log.i("Cal", " removing: start,end day: " + e.startDay + "," + e.endDay
+ + " start,end time: " + e.startTime + "," + e.endTime
+ + " start,end millis: " + e.getStartMillis() + "," + e.getEndMillis()
+ + " " + e.title);
+ }
+ colMask &= ~(1L << active.getColumn());
+ iter.remove();
+ }
+ }
+
+ // If the active list is empty, then reset the max columns, clear
+ // the column bit mask, and empty the groupList.
+ if (activeList.isEmpty()) {
+ for (Event ev : groupList) {
+ ev.setMaxColumns(maxCols);
+ }
+ maxCols = 0;
+ colMask = 0;
+ groupList.clear();
+ }
+
+ // Find the first empty column. Empty columns are represented by
+ // zero bits in the column mask "colMask".
+ int col = findFirstZeroBit(colMask);
+ if (col == 64)
+ col = 63;
+ colMask |= (1L << col);
+ event.setColumn(col);
+ activeList.add(event);
+ groupList.add(event);
+ int len = activeList.size();
+ if (maxCols < len)
+ maxCols = len;
+ }
+ for (Event ev : groupList) {
+ ev.setMaxColumns(maxCols);
+ }
+ }
+
+ public static int findFirstZeroBit(long val) {
+ for (int ii = 0; ii < 64; ++ii) {
+ if ((val & (1L << ii)) == 0)
+ return ii;
+ }
+ return 64;
+ }
+
+ /**
+ * Returns a darker version of the given color. It does this by dividing
+ * each of the red, green, and blue components by 2. The alpha value is
+ * preserved.
+ */
+ private static final int getDarkerColor(int color) {
+ int darker = (color >> 1) & 0x007f7f7f;
+ int alpha = color & 0xff000000;
+ return alpha | darker;
+ }
+
+ // For testing. This method can be removed at any time.
+ private static ArrayList<Event> createTestEventList() {
+ ArrayList<Event> evList = new ArrayList<Event>();
+ createTestEvent(evList, 1, 5, 10);
+ createTestEvent(evList, 2, 5, 10);
+ createTestEvent(evList, 3, 15, 20);
+ createTestEvent(evList, 4, 20, 25);
+ createTestEvent(evList, 5, 30, 70);
+ createTestEvent(evList, 6, 32, 40);
+ createTestEvent(evList, 7, 32, 40);
+ createTestEvent(evList, 8, 34, 38);
+ createTestEvent(evList, 9, 34, 38);
+ createTestEvent(evList, 10, 42, 50);
+ createTestEvent(evList, 11, 45, 60);
+ createTestEvent(evList, 12, 55, 90);
+ createTestEvent(evList, 13, 65, 75);
+
+ createTestEvent(evList, 21, 105, 130);
+ createTestEvent(evList, 22, 110, 120);
+ createTestEvent(evList, 23, 115, 130);
+ createTestEvent(evList, 24, 125, 140);
+ createTestEvent(evList, 25, 127, 135);
+
+ createTestEvent(evList, 31, 150, 160);
+ createTestEvent(evList, 32, 152, 162);
+ createTestEvent(evList, 33, 153, 163);
+ createTestEvent(evList, 34, 155, 170);
+ createTestEvent(evList, 35, 158, 175);
+ createTestEvent(evList, 36, 165, 180);
+
+ return evList;
+ }
+
+ // For testing. This method can be removed at any time.
+ private static Event createTestEvent(ArrayList<Event> evList, int id,
+ int startMinute, int endMinute) {
+ Event ev = new Event();
+ ev.title = "ev" + id;
+ ev.startDay = 1;
+ ev.endDay = 1;
+ ev.setStartMillis(startMinute);
+ ev.setEndMillis(endMinute);
+ evList.add(ev);
+ return ev;
+ }
+
+ public final void dump() {
+ Log.e("Cal", "+-----------------------------------------+");
+ Log.e("Cal", "+ id = " + id);
+ Log.e("Cal", "+ color = " + color);
+ Log.e("Cal", "+ title = " + title);
+ Log.e("Cal", "+ location = " + location);
+ Log.e("Cal", "+ allDay = " + allDay);
+ Log.e("Cal", "+ startDay = " + startDay);
+ Log.e("Cal", "+ endDay = " + endDay);
+ Log.e("Cal", "+ startTime = " + startTime);
+ Log.e("Cal", "+ endTime = " + endTime);
+ }
+
+ public final boolean intersects(int julianDay, int startMinute,
+ int endMinute) {
+ if (endDay < julianDay) {
+ return false;
+ }
+
+ if (startDay > julianDay) {
+ return false;
+ }
+
+ if (endDay == julianDay) {
+ if (endTime < startMinute) {
+ return false;
+ }
+ // An event that ends at the start minute should not be considered
+ // as intersecting the given time span, but don't exclude
+ // zero-length (or very short) events.
+ if (endTime == startMinute
+ && (startTime != endTime || startDay != endDay)) {
+ return false;
+ }
+ }
+
+ if (startDay == julianDay && startTime > endMinute) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the event title and location separated by a comma. If the
+ * location is already part of the title (at the end of the title), then
+ * just the title is returned.
+ *
+ * @return the event title and location as a String
+ */
+ public String getTitleAndLocation() {
+ String text = title.toString();
+
+ // Append the location to the title, unless the title ends with the
+ // location (for example, "meeting in building 42" ends with the
+ // location).
+ if (location != null) {
+ String locationString = location.toString();
+ if (!text.endsWith(locationString)) {
+ text += ", " + locationString;
+ }
+ }
+ return text;
+ }
+
+ public void setColumn(int column) {
+ mColumn = column;
+ }
+
+ public int getColumn() {
+ return mColumn;
+ }
+
+ public void setMaxColumns(int maxColumns) {
+ mMaxColumns = maxColumns;
+ }
+
+ public int getMaxColumns() {
+ return mMaxColumns;
+ }
+
+ public void setStartMillis(long startMillis) {
+ this.startMillis = startMillis;
+ }
+
+ public long getStartMillis() {
+ return startMillis;
+ }
+
+ public void setEndMillis(long endMillis) {
+ this.endMillis = endMillis;
+ }
+
+ public long getEndMillis() {
+ return endMillis;
+ }
+}
diff --git a/src/com/android/calendar/EventGeometry.java b/src/com/android/calendar/EventGeometry.java
new file mode 100644
index 00000000..ebfa6a3e
--- /dev/null
+++ b/src/com/android/calendar/EventGeometry.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2008 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.graphics.Rect;
+
+public class EventGeometry {
+ // This is the space from the grid line to the event rectangle.
+ private int mCellMargin = 0;
+
+ private float mMinuteHeight;
+
+ private float mHourGap;
+ private float mMinEventHeight;
+
+ void setCellMargin(int cellMargin) {
+ mCellMargin = cellMargin;
+ }
+
+ void setHourGap(float gap) {
+ mHourGap = gap;
+ }
+
+ void setMinEventHeight(float height) {
+ mMinEventHeight = height;
+ }
+
+ void setHourHeight(float height) {
+ mMinuteHeight = height / 60.0f;
+ }
+
+ // Computes the rectangle coordinates of the given event on the screen.
+ // Returns true if the rectangle is visible on the screen.
+ boolean computeEventRect(int date, int left, int top, int cellWidth, Event event) {
+ if (event.allDay) {
+ return false;
+ }
+
+ float cellMinuteHeight = mMinuteHeight;
+ int startDay = event.startDay;
+ int endDay = event.endDay;
+
+ if (startDay > date || endDay < date) {
+ return false;
+ }
+
+ int startTime = event.startTime;
+ int endTime = event.endTime;
+
+ // If the event started on a previous day, then show it starting
+ // at the beginning of this day.
+ if (startDay < date) {
+ startTime = 0;
+ }
+
+ // If the event ends on a future day, then show it extending to
+ // the end of this day.
+ if (endDay > date) {
+ endTime = CalendarView.MINUTES_PER_DAY;
+ }
+
+ int col = event.getColumn();
+ int maxCols = event.getMaxColumns();
+ int startHour = startTime / 60;
+ int endHour = endTime / 60;
+
+ // If the end point aligns on a cell boundary then count it as
+ // ending in the previous cell so that we don't cross the border
+ // between hours.
+ if (endHour * 60 == endTime)
+ endHour -= 1;
+
+ event.top = top;
+ event.top += (int) (startTime * cellMinuteHeight);
+ event.top += startHour * mHourGap;
+
+ event.bottom = top;
+ event.bottom += (int) (endTime * cellMinuteHeight);
+ event.bottom += endHour * mHourGap;
+
+ // Make the rectangle be at least mMinEventHeight pixels high
+ if (event.bottom < event.top + mMinEventHeight) {
+ event.bottom = event.top + mMinEventHeight;
+ }
+
+ float colWidth = (float) (cellWidth - 2 * mCellMargin) / (float) maxCols;
+ event.left = left + mCellMargin + col * colWidth;
+ event.right = event.left + colWidth;
+ return true;
+ }
+
+ // Computes the busy bits. For each interval containing "interval" minutes,
+ // the busy bit for that interval is set to 1 if the given event overlaps
+ // that interval.
+ void computeBusyBits(int firstDate, int numDays, byte[][] busyBits, Event event, int interval) {
+ if (event.allDay) {
+ return;
+ }
+
+ int endDate = firstDate + numDays;
+ int startDay = event.startDay;
+ int endDay = event.endDay;
+ if (startDay >= endDate || endDay < firstDate) {
+ return;
+ }
+
+ int startTime = event.startTime;
+
+ int day = startDay;
+
+ // If the event started on a previous day, then show it starting
+ // at the beginning of this day.
+ if (day < firstDate) {
+ day = firstDate;
+ startTime = 0;
+ }
+
+ if (endDay >= endDate) {
+ endDay = endDate - 1;
+ }
+
+ int dayIndex = day - firstDate;
+ while (day <= endDay) {
+ int endTime = event.endTime;
+ // If the event ends on a future day, then show it extending to
+ // the end of this day.
+ if (endDay > day) {
+ endTime = CalendarView.MINUTES_PER_DAY;
+ }
+
+ int startInterval = startTime / interval;
+ int endInterval = (endTime + interval - 1) / interval;
+
+ for (int ii = startInterval; ii < endInterval; ii++) {
+ busyBits[dayIndex][ii] = 1;
+ }
+ day += 1;
+ dayIndex += 1;
+ startTime = 0;
+ }
+ }
+
+ /**
+ * Returns true if this event intersects the selection region.
+ */
+ boolean eventIntersectsSelection(Event event, Rect selection) {
+ if (event.left < selection.right && event.right >= selection.left
+ && event.top < selection.bottom && event.bottom >= selection.top) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Computes the distance from the given point to the given event.
+ */
+ float pointToEvent(float x, float y, Event event) {
+ float left = event.left;
+ float right = event.right;
+ float top = event.top;
+ float bottom = event.bottom;
+
+ if (x >= left) {
+ if (x <= right) {
+ if (y >= top) {
+ if (y <= bottom) {
+ // x,y is inside the event rectangle
+ return 0f;
+ }
+ // x,y is below the event rectangle
+ return y - bottom;
+ }
+ // x,y is above the event rectangle
+ return top - y;
+ }
+
+ // x > right
+ float dx = x - right;
+ if (y < top) {
+ // the upper right corner
+ float dy = top - y;
+ return (float) Math.sqrt(dx * dx + dy * dy);
+ }
+ if (y > bottom) {
+ // the lower right corner
+ float dy = y - bottom;
+ return (float) Math.sqrt(dx * dx + dy * dy);
+ }
+ // x,y is to the right of the event rectangle
+ return dx;
+ }
+ // x < left
+ float dx = left - x;
+ if (y < top) {
+ // the upper left corner
+ float dy = top - y;
+ return (float) Math.sqrt(dx * dx + dy * dy);
+ }
+ if (y > bottom) {
+ // the lower left corner
+ float dy = y - bottom;
+ return (float) Math.sqrt(dx * dx + dy * dy);
+ }
+ // x,y is to the left of the event rectangle
+ return dx;
+ }
+}
diff --git a/src/com/android/calendar/EventInfoActivity.java b/src/com/android/calendar/EventInfoActivity.java
new file mode 100644
index 00000000..0b31b015
--- /dev/null
+++ b/src/com/android/calendar/EventInfoActivity.java
@@ -0,0 +1,724 @@
+/*
+ * Copyright (C) 2007 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 static android.provider.Calendar.EVENT_BEGIN_TIME;
+import static android.provider.Calendar.EVENT_END_TIME;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.PorterDuff;
+import android.net.Uri;
+import android.os.Bundle;
+import android.pim.EventRecurrence;
+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.Reminders;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+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.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class EventInfoActivity extends Activity implements View.OnClickListener,
+ AdapterView.OnItemSelectedListener {
+ private static final int MAX_REMINDERS = 5;
+
+ /**
+ * These are the corresponding indices into the array of strings
+ * "R.array.change_response_labels" in the resource file.
+ */
+ static final int UPDATE_SINGLE = 0;
+ static final int UPDATE_ALL = 1;
+
+ private static final String[] EVENT_PROJECTION = new String[] {
+ Events._ID, // 0 do not remove; used in DeleteEventHelper
+ Events.TITLE, // 1 do not remove; used in DeleteEventHelper
+ Events.RRULE, // 2 do not remove; used in DeleteEventHelper
+ Events.ALL_DAY, // 3 do not remove; used in DeleteEventHelper
+ Events.CALENDAR_ID, // 4 do not remove; used in DeleteEventHelper
+ Events.DTSTART, // 5 do not remove; used in DeleteEventHelper
+ Events._SYNC_ID, // 6 do not remove; used in DeleteEventHelper
+ Events.EVENT_TIMEZONE, // 7 do not remove; used in DeleteEventHelper
+ Events.DESCRIPTION, // 8
+ Events.EVENT_LOCATION, // 9
+ Events.HAS_ALARM, // 10
+ Events.ACCESS_LEVEL, // 11
+ Events.COLOR, // 12
+ };
+ private static final int EVENT_INDEX_ID = 0;
+ private static final int EVENT_INDEX_TITLE = 1;
+ private static final int EVENT_INDEX_RRULE = 2;
+ private static final int EVENT_INDEX_ALL_DAY = 3;
+ private static final int EVENT_INDEX_CALENDAR_ID = 4;
+ private static final int EVENT_INDEX_SYNC_ID = 6;
+ private static final int EVENT_INDEX_EVENT_TIMEZONE = 7;
+ private static final int EVENT_INDEX_DESCRIPTION = 8;
+ private static final int EVENT_INDEX_EVENT_LOCATION = 9;
+ private static final int EVENT_INDEX_HAS_ALARM = 10;
+ private static final int EVENT_INDEX_ACCESS_LEVEL = 11;
+ private static final int EVENT_INDEX_COLOR = 12;
+
+ private static final String[] ATTENDEES_PROJECTION = new String[] {
+ Attendees._ID, // 0
+ Attendees.ATTENDEE_RELATIONSHIP, // 1
+ Attendees.ATTENDEE_STATUS, // 2
+ };
+ private static final int ATTENDEES_INDEX_ID = 0;
+ private static final int ATTENDEES_INDEX_RELATIONSHIP = 1;
+ private static final int ATTENDEES_INDEX_STATUS = 2;
+ private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d";
+
+ static final String[] CALENDARS_PROJECTION = new String[] {
+ Calendars._ID, // 0
+ Calendars.DISPLAY_NAME, // 1
+ };
+ static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
+ static final String CALENDARS_WHERE = Calendars._ID + "=%d";
+
+ private static final String[] REMINDERS_PROJECTION = new String[] {
+ Reminders._ID, // 0
+ Reminders.MINUTES, // 1
+ };
+ private static final int REMINDERS_INDEX_MINUTES = 1;
+ private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=%d AND (" +
+ Reminders.METHOD + "=" + Reminders.METHOD_ALERT + " OR " + Reminders.METHOD + "=" +
+ Reminders.METHOD_DEFAULT + ")";
+
+ private static final int MENU_GROUP_REMINDER = 1;
+ private static final int MENU_GROUP_EDIT = 2;
+ private static final int MENU_GROUP_DELETE = 3;
+
+ private static final int MENU_ADD_REMINDER = 1;
+ private static final int MENU_EDIT = 2;
+ private static final int MENU_DELETE = 3;
+
+ private static final int ATTENDEE_NO_RESPONSE = -1;
+ private static final int[] ATTENDEE_VALUES = {
+ ATTENDEE_NO_RESPONSE,
+ Attendees.ATTENDEE_STATUS_ACCEPTED,
+ Attendees.ATTENDEE_STATUS_TENTATIVE,
+ Attendees.ATTENDEE_STATUS_DECLINED,
+ };
+
+ private LinearLayout mRemindersContainer;
+
+ private Uri mUri;
+ private long mEventId;
+ private Cursor mEventCursor;
+ private Cursor mAttendeesCursor;
+ private Cursor mCalendarsCursor;
+
+ private long mStartMillis;
+ private long mEndMillis;
+ private int mVisibility = Calendars.NO_ACCESS;
+ private int mRelationship = Attendees.RELATIONSHIP_ORGANIZER;
+
+ private ArrayList<Integer> mOriginalMinutes = new ArrayList<Integer>();
+ private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0);
+ private ArrayList<Integer> mReminderValues;
+ private ArrayList<String> mReminderLabels;
+ private int mDefaultReminderMinutes;
+
+ private DeleteEventHelper mDeleteEventHelper;
+ private EditResponseHelper mEditResponseHelper;
+
+ private int mResponseOffset;
+ private int mOriginalAttendeeResponse;
+ private boolean mIsRepeating;
+
+ // This is called when one of the "remove reminder" buttons is selected.
+ public void onClick(View v) {
+ LinearLayout reminderItem = (LinearLayout) v.getParent();
+ LinearLayout parent = (LinearLayout) reminderItem.getParent();
+ parent.removeView(reminderItem);
+ mReminderItems.remove(reminderItem);
+ updateRemindersVisibility();
+ }
+
+ public void onItemSelected(AdapterView parent, View v, int position, long id) {
+ // If they selected the "No response" option, then don't display the
+ // dialog asking which events to change.
+ if (id == 0 && mResponseOffset == 0) {
+ return;
+ }
+
+ // If this is not a repeating event, then don't display the dialog
+ // asking which events to change.
+ if (!mIsRepeating) {
+ return;
+ }
+
+ // If the selection is the same as the original, then don't display the
+ // dialog asking which events to change.
+ int index = findResponseIndexFor(mOriginalAttendeeResponse);
+ if (position == index + mResponseOffset) {
+ return;
+ }
+
+ // This is a repeating event. We need to ask the user if they mean to
+ // change just this one instance or all instances.
+ mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents());
+ }
+
+ public void onNothingSelected(AdapterView parent) {
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ // Event cursor
+ Intent intent = getIntent();
+ mUri = intent.getData();
+ ContentResolver cr = getContentResolver();
+ mStartMillis = intent.getLongExtra(EVENT_BEGIN_TIME, 0);
+ mEndMillis = intent.getLongExtra(EVENT_END_TIME, 0);
+ mEventCursor = managedQuery(mUri, EVENT_PROJECTION, null, null);
+ if (initEventCursor()) {
+ // The cursor is empty. This can happen if the event was deleted.
+ finish();
+ return;
+ }
+
+ setContentView(R.layout.event_info_activity);
+
+ // Attendees cursor
+ Uri uri = Attendees.CONTENT_URI;
+ String where = String.format(ATTENDEES_WHERE, mEventId);
+ mAttendeesCursor = managedQuery(uri, ATTENDEES_PROJECTION, where, null);
+ initAttendeesCursor();
+
+ // Calendars cursor
+ uri = Calendars.CONTENT_URI;
+ where = String.format(CALENDARS_WHERE, mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID));
+ mCalendarsCursor = managedQuery(uri, CALENDARS_PROJECTION, where, null);
+ initCalendarsCursor();
+
+ Resources res = getResources();
+
+ if (mVisibility >= Calendars.CONTRIBUTOR_ACCESS &&
+ mRelationship == Attendees.RELATIONSHIP_ATTENDEE) {
+ setTitle(res.getString(R.string.event_info_title_invite));
+ } else {
+ setTitle(res.getString(R.string.event_info_title));
+ }
+
+ // Initialize the reminder values array.
+ Resources r = getResources();
+ String[] strings = r.getStringArray(R.array.reminder_minutes_values);
+ int size = strings.length;
+ ArrayList<Integer> list = new ArrayList<Integer>(size);
+ for (int i = 0 ; i < size ; i++) {
+ list.add(Integer.parseInt(strings[i]));
+ }
+ mReminderValues = list;
+ String[] labels = r.getStringArray(R.array.reminder_minutes_labels);
+ mReminderLabels = new ArrayList<String>(Arrays.asList(labels));
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ String durationString =
+ prefs.getString(CalendarPreferenceActivity.KEY_DEFAULT_REMINDER, "0");
+ mDefaultReminderMinutes = Integer.parseInt(durationString);
+
+ mRemindersContainer = (LinearLayout) findViewById(R.id.reminders_container);
+
+ // Reminders cursor
+ boolean hasAlarm = mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0;
+ if (hasAlarm) {
+ uri = Reminders.CONTENT_URI;
+ where = String.format(REMINDERS_WHERE, mEventId);
+ Cursor reminderCursor = cr.query(uri, REMINDERS_PROJECTION, where, null, null);
+ try {
+ // First pass: collect all the custom reminder minutes (e.g.,
+ // a reminder of 8 minutes) into a global list.
+ while (reminderCursor.moveToNext()) {
+ int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
+ EditEvent.addMinutesToList(this, mReminderValues, mReminderLabels, minutes);
+ }
+
+ // Second pass: create the reminder spinners
+ reminderCursor.moveToPosition(-1);
+ while (reminderCursor.moveToNext()) {
+ int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
+ mOriginalMinutes.add(minutes);
+ EditEvent.addReminder(this, this, mReminderItems, mReminderValues,
+ mReminderLabels, minutes);
+ }
+ } finally {
+ reminderCursor.close();
+ }
+ }
+
+ updateView();
+ updateRemindersVisibility();
+
+ mDeleteEventHelper = new DeleteEventHelper(this, true /* exit when done */);
+ mEditResponseHelper = new EditResponseHelper(this);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (initEventCursor()) {
+ // The cursor is empty. This can happen if the event was deleted.
+ finish();
+ return;
+ }
+ initAttendeesCursor();
+ initCalendarsCursor();
+ }
+
+ /**
+ * Initializes the event cursor, which is expected to point to the first
+ * (and only) result from a query.
+ * @return true if the cursor is empty.
+ */
+ private boolean initEventCursor() {
+ if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) {
+ return true;
+ }
+ mEventCursor.moveToFirst();
+ mVisibility = mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL);
+ mEventId = mEventCursor.getInt(EVENT_INDEX_ID);
+ String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
+ mIsRepeating = (rRule != null);
+ return false;
+ }
+
+ private void initAttendeesCursor() {
+ if (mAttendeesCursor != null) {
+ if (mAttendeesCursor.moveToFirst()) {
+ mRelationship = mAttendeesCursor.getInt(ATTENDEES_INDEX_RELATIONSHIP);
+ }
+ }
+ }
+
+ private void initCalendarsCursor() {
+ if (mCalendarsCursor != null) {
+ mCalendarsCursor.moveToFirst();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (!isFinishing()) {
+ return;
+ }
+ ContentResolver cr = getContentResolver();
+ ArrayList<Integer> reminderMinutes = EditEvent.reminderItemsToMinutes(mReminderItems,
+ mReminderValues);
+ boolean changed = EditEvent.saveReminders(cr, mEventId, reminderMinutes, mOriginalMinutes);
+ changed |= saveResponse(cr);
+ if (changed) {
+ Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuItem item;
+ item = menu.add(MENU_GROUP_REMINDER, MENU_ADD_REMINDER, 0,
+ R.string.add_new_reminder);
+ item.setIcon(R.drawable.ic_menu_reminder);
+ item.setAlphabeticShortcut('r');
+
+ item = menu.add(MENU_GROUP_EDIT, MENU_EDIT, 0, R.string.edit_event_label);
+ item.setIcon(android.R.drawable.ic_menu_edit);
+ item.setAlphabeticShortcut('e');
+
+ item = menu.add(MENU_GROUP_DELETE, MENU_DELETE, 0, R.string.delete_event_label);
+ item.setIcon(android.R.drawable.ic_menu_delete);
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ // Cannot add reminders to a shared calendar with only free/busy
+ // permissions
+ if (mVisibility >= Calendars.READ_ACCESS && mReminderItems.size() < MAX_REMINDERS) {
+ menu.setGroupVisible(MENU_GROUP_REMINDER, true);
+ menu.setGroupEnabled(MENU_GROUP_REMINDER, true);
+ } else {
+ menu.setGroupVisible(MENU_GROUP_REMINDER, false);
+ menu.setGroupEnabled(MENU_GROUP_REMINDER, false);
+ }
+
+ if (mVisibility >= Calendars.CONTRIBUTOR_ACCESS &&
+ mRelationship >= Attendees.RELATIONSHIP_ORGANIZER) {
+ menu.setGroupVisible(MENU_GROUP_EDIT, true);
+ menu.setGroupEnabled(MENU_GROUP_EDIT, true);
+ menu.setGroupVisible(MENU_GROUP_DELETE, true);
+ menu.setGroupEnabled(MENU_GROUP_DELETE, true);
+ } else {
+ menu.setGroupVisible(MENU_GROUP_EDIT, false);
+ menu.setGroupEnabled(MENU_GROUP_EDIT, false);
+ menu.setGroupVisible(MENU_GROUP_DELETE, false);
+ menu.setGroupEnabled(MENU_GROUP_DELETE, false);
+ }
+
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ super.onOptionsItemSelected(item);
+ switch (item.getItemId()) {
+ case MENU_ADD_REMINDER:
+ // TODO: when adding a new reminder, make it different from the
+ // last one in the list (if any).
+ if (mDefaultReminderMinutes == 0) {
+ EditEvent.addReminder(this, this, mReminderItems,
+ mReminderValues, mReminderLabels, 10 /* minutes */);
+ } else {
+ EditEvent.addReminder(this, this, mReminderItems,
+ mReminderValues, mReminderLabels, mDefaultReminderMinutes);
+ }
+ updateRemindersVisibility();
+ break;
+ case MENU_EDIT:
+ doEdit();
+ break;
+ case MENU_DELETE:
+ doDelete();
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_DEL) {
+ doDelete();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private void updateRemindersVisibility() {
+ if (mReminderItems.size() == 0) {
+ mRemindersContainer.setVisibility(View.GONE);
+ } else {
+ mRemindersContainer.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Saves the response to an invitation if the user changed the response.
+ * Returns true if the database was updated.
+ *
+ * @param cr the ContentResolver
+ * @return true if the database was changed
+ */
+ private boolean saveResponse(ContentResolver cr) {
+ if (mAttendeesCursor == null || mEventCursor == null) {
+ return false;
+ }
+ Spinner spinner = (Spinner) findViewById(R.id.response_value);
+ int position = spinner.getSelectedItemPosition() - mResponseOffset;
+ if (position <= 0) {
+ return false;
+ }
+
+ int status = ATTENDEE_VALUES[position];
+
+ // If the status has not changed, then don't update the database
+ if (status == mOriginalAttendeeResponse) {
+ return false;
+ }
+
+ long attendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID);
+ if (!mIsRepeating) {
+ // This is a non-repeating event
+ updateResponse(cr, mEventId, attendeeId, status);
+ return true;
+ }
+
+ // This is a repeating event
+ int whichEvents = mEditResponseHelper.getWhichEvents();
+ switch (whichEvents) {
+ case -1:
+ return false;
+ case UPDATE_SINGLE:
+ createExceptionResponse(cr, mEventId, attendeeId, status);
+ return true;
+ case UPDATE_ALL:
+ updateResponse(cr, mEventId, attendeeId, status);
+ return true;
+ default:
+ Log.e("Calendar", "Unexpected choice for updating invitation response");
+ break;
+ }
+ return false;
+ }
+
+ private void updateResponse(ContentResolver cr, long eventId, long attendeeId, int status) {
+ // Update the "selfAttendeeStatus" field for the event
+ ContentValues values = new ContentValues();
+
+ // Will need to add email when MULTIPLE_ATTENDEES_PER_EVENT supported.
+ values.put(Attendees.ATTENDEE_STATUS, status);
+ values.put(Attendees.EVENT_ID, eventId);
+
+ Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, attendeeId);
+ cr.update(uri, values, null /* where */, null /* selection args */);
+ }
+
+ private void createExceptionResponse(ContentResolver cr, long eventId,
+ long attendeeId, int status) {
+ // Fetch information about the repeating event.
+ Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
+ Cursor cursor = cr.query(uri, EVENT_PROJECTION, null, null, null);
+ if (cursor == null) {
+ return;
+ }
+
+ try {
+ cursor.moveToFirst();
+ ContentValues values = new ContentValues();
+
+ String title = cursor.getString(EVENT_INDEX_TITLE);
+ String timezone = cursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
+ int calendarId = cursor.getInt(EVENT_INDEX_CALENDAR_ID);
+ boolean allDay = cursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
+ String syncId = cursor.getString(EVENT_INDEX_SYNC_ID);
+
+ values.put(Events.TITLE, title);
+ values.put(Events.EVENT_TIMEZONE, timezone);
+ values.put(Events.ALL_DAY, allDay ? 1 : 0);
+ values.put(Events.CALENDAR_ID, calendarId);
+ values.put(Events.DTSTART, mStartMillis);
+ values.put(Events.DTEND, mEndMillis);
+ values.put(Events.ORIGINAL_EVENT, syncId);
+ values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
+ values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
+ values.put(Events.STATUS, Events.STATUS_CONFIRMED);
+ values.put(Events.SELF_ATTENDEE_STATUS, status);
+
+ // Create a recurrence exception
+ Uri newUri = cr.insert(Events.CONTENT_URI, values);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private int findResponseIndexFor(int response) {
+ int size = ATTENDEE_VALUES.length;
+ for (int index = 0; index < size; index++) {
+ if (ATTENDEE_VALUES[index] == response) {
+ return index;
+ }
+ }
+ return 0;
+ }
+
+ private void doEdit() {
+ Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
+ Intent intent = new Intent(Intent.ACTION_EDIT, uri);
+ intent.putExtra(Calendar.EVENT_BEGIN_TIME, mStartMillis);
+ intent.putExtra(Calendar.EVENT_END_TIME, mEndMillis);
+ intent.setClass(EventInfoActivity.this, EditEvent.class);
+ startActivity(intent);
+ finish();
+ }
+
+ private void doDelete() {
+ mDeleteEventHelper.delete(mStartMillis, mEndMillis, mEventCursor, -1);
+ }
+
+ private void updateView() {
+ if (mEventCursor == null) {
+ return;
+ }
+ Resources res = getResources();
+ ContentResolver cr = getContentResolver();
+
+ String eventName = mEventCursor.getString(EVENT_INDEX_TITLE);
+ if (eventName == null || eventName.length() == 0) {
+ eventName = res.getString(R.string.no_title_label);
+ }
+
+ boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
+ String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION);
+ String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION);
+ String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
+ boolean hasAlarm = mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0;
+ String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
+ int color = mEventCursor.getInt(EVENT_INDEX_COLOR) & 0xbbffffff;
+
+ View strip = (View) findViewById(R.id.strip);
+ strip.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
+
+ TextView title = (TextView) findViewById(R.id.title);
+ title.setTextColor(color);
+
+ View divider = (View) findViewById(R.id.divider);
+ divider.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
+
+ // What
+ if (eventName != null) {
+ setTextCommon(R.id.title, eventName);
+ }
+
+ // When
+ String when;
+ int flags;
+ if (allDay) {
+ flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE;
+ } else {
+ flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE;
+ if (DateFormat.is24HourFormat(this)) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ }
+ }
+ when = DateUtils.formatDateRange(this, mStartMillis, mEndMillis, flags);
+ setTextCommon(R.id.when, when);
+
+ // Show the event timezone if it is different from the local timezone
+ Time time = new Time();
+ String localTimezone = time.timezone;
+ if (allDay) {
+ localTimezone = Time.TIMEZONE_UTC;
+ }
+ if (eventTimezone != null && !localTimezone.equals(eventTimezone) && !allDay) {
+ setTextCommon(R.id.timezone, localTimezone);
+ } else {
+ setVisibilityCommon(R.id.timezone_container, View.GONE);
+ }
+
+ // Repeat
+ if (rRule != null) {
+ EventRecurrence eventRecurrence = new EventRecurrence();
+ eventRecurrence.parse(rRule);
+ Time date = new Time();
+ if (allDay) {
+ date.timezone = Time.TIMEZONE_UTC;
+ }
+ date.set(mStartMillis);
+ eventRecurrence.setStartDate(date);
+ String repeatString = eventRecurrence.getRepeatString();
+ setTextCommon(R.id.repeat, repeatString);
+ } else {
+ setVisibilityCommon(R.id.repeat_container, View.GONE);
+ }
+
+ // Where
+ if (location == null || location.length() == 0) {
+ setVisibilityCommon(R.id.where, View.GONE);
+ } else {
+ setTextCommon(R.id.where, location);
+ }
+
+ // Description
+ if (description == null || description.length() == 0) {
+ setVisibilityCommon(R.id.description, View.GONE);
+ } else {
+ setTextCommon(R.id.description, description);
+ }
+
+ // Calendar
+ if (mCalendarsCursor != null) {
+ mCalendarsCursor.moveToFirst();
+ String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
+ setTextCommon(R.id.calendar, calendarName);
+ } else {
+ setVisibilityCommon(R.id.calendar_container, View.GONE);
+ }
+
+ // Response
+ updateResponse();
+ }
+
+ void updateResponse() {
+ if (mVisibility < Calendars.CONTRIBUTOR_ACCESS ||
+ mRelationship != Attendees.RELATIONSHIP_ATTENDEE) {
+ setVisibilityCommon(R.id.response_container, View.GONE);
+ return;
+ }
+
+ setVisibilityCommon(R.id.response_container, View.VISIBLE);
+
+ Spinner spinner = (Spinner) findViewById(R.id.response_value);
+
+ mOriginalAttendeeResponse = ATTENDEE_NO_RESPONSE;
+ if (mAttendeesCursor != null) {
+ mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
+ }
+ mResponseOffset = 0;
+
+ /* If the user has previously responded to this event
+ * we should not allow them to select no response again.
+ * Switch the entries to a set of entries without the
+ * no response option.
+ */
+ if ((mOriginalAttendeeResponse != Attendees.ATTENDEE_STATUS_INVITED)
+ && (mOriginalAttendeeResponse != ATTENDEE_NO_RESPONSE)
+ && (mOriginalAttendeeResponse != Attendees.ATTENDEE_STATUS_NONE)) {
+ CharSequence[] entries;
+ entries = getResources().getTextArray(R.array.response_labels2);
+ mResponseOffset = -1;
+ ArrayAdapter<CharSequence> adapter =
+ new ArrayAdapter<CharSequence>(this,
+ android.R.layout.simple_spinner_item, entries);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinner.setAdapter(adapter);
+ }
+
+ int index = findResponseIndexFor(mOriginalAttendeeResponse);
+ spinner.setSelection(index + mResponseOffset);
+ spinner.setOnItemSelectedListener(this);
+ }
+
+ private void setTextCommon(int id, CharSequence text) {
+ TextView textView = (TextView) findViewById(id);
+ if (textView == null)
+ return;
+ textView.setText(text);
+ }
+
+ private void setVisibilityCommon(int id, int visibility) {
+ View v = findViewById(id);
+ if (v != null) {
+ v.setVisibility(visibility);
+ }
+ return;
+ }
+}
diff --git a/src/com/android/calendar/EventLoader.java b/src/com/android/calendar/EventLoader.java
new file mode 100644
index 00000000..eb8a82d7
--- /dev/null
+++ b/src/com/android/calendar/EventLoader.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2008 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.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Handler;
+import android.os.Process;
+import android.provider.Calendar.BusyBits;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class EventLoader {
+
+ private Context mContext;
+ private Handler mHandler = new Handler();
+ private AtomicInteger mSequenceNumber = new AtomicInteger();
+
+ private LinkedBlockingQueue<LoadRequest> mLoaderQueue;
+ private LoaderThread mLoaderThread;
+ private ContentResolver mResolver;
+
+ private static interface LoadRequest {
+ public void processRequest(EventLoader eventLoader);
+ public void skipRequest(EventLoader eventLoader);
+ }
+
+ private static class ShutdownRequest implements LoadRequest {
+ public void processRequest(EventLoader eventLoader) {
+ }
+
+ public void skipRequest(EventLoader eventLoader) {
+ }
+ }
+
+ private static class LoadBusyBitsRequest implements LoadRequest {
+ public int startDay;
+ public int numDays;
+ public int[] busybits;
+ public int[] allDayCounts;
+ public Runnable uiCallback;
+
+ public LoadBusyBitsRequest(int startDay, int numDays, int[] busybits, int[] allDayCounts,
+ final Runnable uiCallback) {
+ this.startDay = startDay;
+ this.numDays = numDays;
+ this.busybits = busybits;
+ this.allDayCounts = allDayCounts;
+ this.uiCallback = uiCallback;
+ }
+
+ public void processRequest(EventLoader eventLoader) {
+ final Handler handler = eventLoader.mHandler;
+ ContentResolver cr = eventLoader.mResolver;
+
+ // Clear the busy bits and all-day counts
+ for (int dayIndex = 0; dayIndex < numDays; dayIndex++) {
+ busybits[dayIndex] = 0;
+ allDayCounts[dayIndex] = 0;
+ }
+
+ Cursor cursor = BusyBits.query(cr, startDay, numDays);
+ try {
+ int dayColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.DAY);
+ int busybitColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.BUSYBITS);
+ int allDayCountColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.ALL_DAY_COUNT);
+
+ while (cursor.moveToNext()) {
+ int day = cursor.getInt(dayColumnIndex);
+ int dayIndex = day - startDay;
+ busybits[dayIndex] = cursor.getInt(busybitColumnIndex);
+ allDayCounts[dayIndex] = cursor.getInt(allDayCountColumnIndex);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ handler.post(uiCallback);
+ }
+
+ public void skipRequest(EventLoader eventLoader) {
+ }
+ }
+
+ private static class LoadEventsRequest implements LoadRequest {
+
+ public int id;
+ public long startMillis;
+ public int numDays;
+ public ArrayList<Event> events;
+ public Runnable successCallback;
+ public Runnable cancelCallback;
+
+ public LoadEventsRequest(int id, long startMillis, int numDays, ArrayList<Event> events,
+ final Runnable successCallback, final Runnable cancelCallback) {
+ this.id = id;
+ this.startMillis = startMillis;
+ this.numDays = numDays;
+ this.events = events;
+ this.successCallback = successCallback;
+ this.cancelCallback = cancelCallback;
+ }
+
+ public void processRequest(EventLoader eventLoader) {
+ Event.loadEvents(eventLoader.mContext, events, startMillis,
+ numDays, id, eventLoader.mSequenceNumber);
+
+ // Check if we are still the most recent request.
+ if (id == eventLoader.mSequenceNumber.get()) {
+ eventLoader.mHandler.post(successCallback);
+ } else {
+ eventLoader.mHandler.post(cancelCallback);
+ }
+ }
+
+ public void skipRequest(EventLoader eventLoader) {
+ eventLoader.mHandler.post(cancelCallback);
+ }
+ }
+
+ private static class LoaderThread extends Thread {
+ LinkedBlockingQueue<LoadRequest> mQueue;
+ EventLoader mEventLoader;
+
+ public LoaderThread(LinkedBlockingQueue<LoadRequest> queue, EventLoader eventLoader) {
+ mQueue = queue;
+ mEventLoader = eventLoader;
+ }
+
+ public void shutdown() {
+ try {
+ mQueue.put(new ShutdownRequest());
+ } catch (InterruptedException ex) {
+ // The put() method fails with InterruptedException if the
+ // queue is full. This should never happen because the queue
+ // has no limit.
+ Log.e("Cal", "LoaderThread.shutdown() interrupted!");
+ }
+ }
+
+ @Override
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ while (true) {
+ try {
+ // Wait for the next request
+ LoadRequest request = mQueue.take();
+
+ // If there are a bunch of requests already waiting, then
+ // skip all but the most recent request.
+ while (!mQueue.isEmpty()) {
+ // Let the request know that it was skipped
+ request.skipRequest(mEventLoader);
+
+ // Skip to the next request
+ request = mQueue.take();
+ }
+
+ if (request instanceof ShutdownRequest) {
+ return;
+ }
+ request.processRequest(mEventLoader);
+ } catch (InterruptedException ex) {
+ Log.e("Cal", "background LoaderThread interrupted!");
+ }
+ }
+ }
+ }
+
+ public EventLoader(Context context) {
+ mContext = context;
+ mLoaderQueue = new LinkedBlockingQueue<LoadRequest>();
+ mResolver = context.getContentResolver();
+ }
+
+ /**
+ * Call this from the activity's onResume()
+ */
+ public void startBackgroundThread() {
+ mLoaderThread = new LoaderThread(mLoaderQueue, this);
+ mLoaderThread.start();
+ }
+
+ /**
+ * Call this from the activity's onPause()
+ */
+ public void stopBackgroundThread() {
+ mLoaderThread.shutdown();
+ }
+
+ /**
+ * Loads "numDays" days worth of events, starting at start, into events.
+ * Posts uiCallback to the {@link Handler} for this view, which will run in the UI thread.
+ * Reuses an existing background thread, if events were already being loaded in the background.
+ * NOTE: events and uiCallback are not used if an existing background thread gets reused --
+ * the ones that were passed in on the call that results in the background thread getting
+ * created are used, and the most recent call's worth of data is loaded into events and posted
+ * via the uiCallback.
+ */
+ void loadEventsInBackground(final int numDays, final ArrayList<Event> events,
+ long start, final Runnable successCallback, final Runnable cancelCallback) {
+
+ // Increment the sequence number for requests. We don't care if the
+ // sequence numbers wrap around because we test for equality with the
+ // latest one.
+ int id = mSequenceNumber.incrementAndGet();
+
+ // Send the load request to the background thread
+ LoadEventsRequest request = new LoadEventsRequest(id, start, numDays,
+ events, successCallback, cancelCallback);
+
+ try {
+ mLoaderQueue.put(request);
+ } catch (InterruptedException ex) {
+ // The put() method fails with InterruptedException if the
+ // queue is full. This should never happen because the queue
+ // has no limit.
+ Log.e("Cal", "loadEventsInBackground() interrupted!");
+ }
+ }
+
+ void loadBusyBitsInBackground(int startDay, int numDays, int[] busybits, int[] allDayCounts,
+ final Runnable uiCallback) {
+ // Send the load request to the background thread
+ LoadBusyBitsRequest request = new LoadBusyBitsRequest(startDay, numDays, busybits,
+ allDayCounts, uiCallback);
+
+ try {
+ mLoaderQueue.put(request);
+ } catch (InterruptedException ex) {
+ // The put() method fails with InterruptedException if the
+ // queue is full. This should never happen because the queue
+ // has no limit.
+ Log.e("Cal", "loadBusyBitsInBackground() interrupted!");
+ }
+ }
+}
diff --git a/src/com/android/calendar/IcsImportActivity.java b/src/com/android/calendar/IcsImportActivity.java
new file mode 100644
index 00000000..c4f5fdea
--- /dev/null
+++ b/src/com/android/calendar/IcsImportActivity.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2007 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.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.pim.ICalendar;
+import android.provider.Calendar;
+import android.util.Config;
+import android.util.Log;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+public class IcsImportActivity extends Activity {
+
+ private static final String TAG = "Calendar";
+
+ // TODO: consolidate this code with the EventActivity
+ private static class CalendarInfo {
+ public final long id;
+ public final String name;
+
+ public CalendarInfo(long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+ private View mView;
+ private Button mImportButton;
+ private Button mCancelButton;
+ private Spinner mCalendars;
+ private ImageView mCalendarIcon;
+ private TextView mNumEvents;
+
+ private ICalendar.Component mCalendar = null;
+
+ private View.OnClickListener mImportListener = new View.OnClickListener() {
+ public void onClick(View v) {
+ importCalendar();
+ finish();
+ }
+ };
+
+ private View.OnClickListener mCancelListener = new View.OnClickListener() {
+ public void onClick(View v) {
+ finish();
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.ics_import_activity);
+ mView = findViewById(R.id.import_ics);
+
+ mCalendarIcon = (ImageView) findViewById(R.id.calendar_icon);
+ mCalendars = (Spinner) findViewById(R.id.calendars);
+ populateCalendars();
+
+ mImportButton = (Button) findViewById(R.id.import_button);
+ mImportButton.setOnClickListener(mImportListener);
+ mCancelButton = (Button) findViewById(R.id.cancel_button);
+ mCancelButton.setOnClickListener(mCancelListener);
+
+ mNumEvents = (TextView) findViewById(R.id.num_events);
+
+ Intent intent = getIntent();
+ String data = intent.getStringExtra("ics");
+ if (data == null) {
+ Uri content = intent.getData();
+ if (content != null) {
+ InputStream is = null;
+ try {
+ is = getContentResolver().openInputStream(content);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte[] buf = new byte[8096];
+ int bytesRead = -1;
+ int pos = 0;
+ while ((bytesRead = is.read(buf)) != -1) {
+ baos.write(buf, pos, bytesRead);
+ pos += bytesRead;
+ }
+ data = new String(baos.toByteArray(), "UTF-8");
+ } catch (FileNotFoundException fnfe) {
+ Log.w(TAG, "Could not open data.", fnfe);
+ } catch (IOException ioe) {
+ Log.w(TAG, "Could not read data.", ioe);
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException ioe) {
+ Log.w(TAG, "Could not close InputStream.", ioe);
+ }
+ }
+ }
+ }
+ }
+ if (data == null) {
+ Log.w(TAG, "No iCalendar data to parse.");
+ finish();
+ return;
+ }
+ parseCalendar(data);
+ }
+
+ private void populateCalendars() {
+ ContentResolver cr = getContentResolver();
+ Cursor c = cr.query(Calendar.Calendars.CONTENT_URI,
+ new String[] { Calendar.Calendars._ID,
+ Calendar.Calendars.DISPLAY_NAME,
+ Calendar.Calendars.SELECTED,
+ Calendar.Calendars.ACCESS_LEVEL },
+ Calendar.Calendars.SELECTED + "=1 AND "
+ + Calendar.Calendars.ACCESS_LEVEL + ">="
+ + Calendar.Calendars.CONTRIBUTOR_ACCESS,
+ null, null /* sort order */);
+
+ ArrayList<CalendarInfo> items = new ArrayList<CalendarInfo>();
+ try {
+ // TODO: write a custom adapter that wraps the cursor?
+ int idColumn = c.getColumnIndex(Calendar.Calendars._ID);
+ int nameColumn = c.getColumnIndex(Calendar.Calendars.DISPLAY_NAME);
+ while (c.moveToNext()) {
+ long id = c.getLong(idColumn);
+ String name = c.getString(nameColumn);
+ items.add(new CalendarInfo(id, name));
+ }
+ } finally {
+ c.deactivate();
+ }
+
+ mCalendars.setAdapter(new ArrayAdapter<CalendarInfo>(this,
+ android.R.layout.simple_spinner_item, items));
+ }
+
+ private void parseCalendar(String data) {
+ mCalendar = null;
+ try {
+ mCalendar = ICalendar.parseCalendar(data);
+ } catch (ICalendar.FormatException fe) {
+ if (Config.LOGD) {
+ Log.d(TAG, "Could not parse iCalendar.", fe);
+ // TODO: show an error message.
+ finish();
+ return;
+ }
+ }
+ if (mCalendar.getComponents() == null) {
+ Log.d(TAG, "No events in iCalendar.");
+ finish();
+ return;
+ }
+ int numEvents = 0;
+ for (ICalendar.Component component : mCalendar.getComponents()) {
+ if ("VEVENT".equals(component.getName())) {
+ // TODO: display a list of the events (start time, title) in
+ // the UI?
+ ++numEvents;
+ }
+ }
+ // TODO: special-case a single-event calendar. switch to the
+ // EventActivity, once the EventActivity supports displaying data that
+ // is passed in via the extras.
+ // OR, we could flip things around, where the EventActivity handles ICS
+ // import by default, and delegates to the IcsImportActivity if it finds
+ // that there are more than one event in the iCalendar. that would
+ // avoid an extra activity launch for the expected common case of
+ // importing a single event.
+ mNumEvents.setText(Integer.toString(numEvents));
+ }
+
+ private void importCalendar() {
+
+ ContentResolver cr = getContentResolver();
+
+ int numImported = 0;
+ ContentValues values = new ContentValues();
+
+ for (ICalendar.Component component : mCalendar.getComponents()) {
+ if ("VEVENT".equals(component.getName())) {
+ CalendarInfo calInfo =
+ (CalendarInfo) mCalendars.getSelectedItem();
+ if (Calendar.Events.insertVEvent(cr, component, calInfo.id,
+ Calendar.Events.STATUS_CONFIRMED, values) != null) {
+ ++numImported;
+ }
+ }
+ }
+ // TODO: display how many were imported.
+ }
+}
diff --git a/src/com/android/calendar/LaunchActivity.java b/src/com/android/calendar/LaunchActivity.java
new file mode 100644
index 00000000..4dabc5c7
--- /dev/null
+++ b/src/com/android/calendar/LaunchActivity.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2007 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.google.android.googlelogin.GoogleLoginServiceConstants;
+import com.google.android.googlelogin.GoogleLoginServiceHelper;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.Gmail;
+
+public class LaunchActivity extends Activity {
+
+ // An arbitrary constant to pass to the GoogleLoginHelperService
+ private static final int GET_ACCOUNT_REQUEST = 1;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ // Our UI is not something intended for the user to see. We just
+ // stick around until we can figure out what to do next based on
+ // the current state of the system.
+ setVisible(false);
+
+ // Only try looking for an account if this is the first launch.
+ if (icicle == null) {
+ // This will request a Gmail account and if none are present, it will
+ // invoke SetupWizard to login or create one. The result is returned
+ // through onActivityResult().
+ Bundle bundle = new Bundle();
+ bundle.putCharSequence("optional_message", getText(R.string.calendar_plug));
+ GoogleLoginServiceHelper.getCredentials(
+ this,
+ GET_ACCOUNT_REQUEST,
+ bundle,
+ GoogleLoginServiceConstants.PREFER_HOSTED,
+ Gmail.GMAIL_AUTH_SERVICE,
+ true);
+ }
+ }
+
+ private void onAccountsLoaded(String account) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ String startActivity = prefs.getString(CalendarPreferenceActivity.KEY_START_VIEW,
+ CalendarPreferenceActivity.DEFAULT_START_VIEW);
+
+ // Get the data for from this intent, if any
+ Intent myIntent = getIntent();
+ Uri myData = myIntent.getData();
+
+ // Set up the intent for the start activity
+ Intent intent = new Intent();
+ if (myData != null) {
+ intent.setData(myData);
+ }
+ intent.setClassName(this, startActivity);
+ startActivity(intent);
+ finish();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ super.onActivityResult(requestCode, resultCode, intent);
+ if (requestCode == GET_ACCOUNT_REQUEST) {
+ if (resultCode == RESULT_OK) {
+ if (intent != null) {
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ final String account;
+ account = extras.getString(GoogleLoginServiceConstants.AUTH_ACCOUNT_KEY);
+ onAccountsLoaded(account);
+ }
+ }
+ } else {
+ finish();
+ }
+ }
+ }
+}
diff --git a/src/com/android/calendar/MenuHelper.java b/src/com/android/calendar/MenuHelper.java
new file mode 100644
index 00000000..f23cbf97
--- /dev/null
+++ b/src/com/android/calendar/MenuHelper.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2008 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 static android.provider.Calendar.EVENT_BEGIN_TIME;
+import static android.provider.Calendar.EVENT_END_TIME;
+import android.app.Activity;
+import android.content.Intent;
+import android.text.format.DateUtils;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class MenuHelper {
+ private static final int MENU_GROUP_AGENDA = 1;
+ private static final int MENU_GROUP_DAY = 2;
+ private static final int MENU_GROUP_WEEK = 3;
+ private static final int MENU_GROUP_MONTH = 4;
+ private static final int MENU_GROUP_EVENT_CREATE = 5;
+ private static final int MENU_GROUP_TODAY = 6;
+ private static final int MENU_GROUP_SELECT_CALENDARS = 7;
+ private static final int MENU_GROUP_PREFERENCES = 8;
+
+ public static final int MENU_GOTO_TODAY = 1;
+ public static final int MENU_AGENDA = 2;
+ public static final int MENU_DAY = 3;
+ public static final int MENU_WEEK = 4;
+ public static final int MENU_EVENT_VIEW = 5;
+ public static final int MENU_EVENT_CREATE = 6;
+ public static final int MENU_EVENT_EDIT = 7;
+ public static final int MENU_EVENT_DELETE = 8;
+ public static final int MENU_MONTH = 9;
+ public static final int MENU_SELECT_CALENDARS = 10;
+ public static final int MENU_PREFERENCES = 11;
+
+ public static void onPrepareOptionsMenu(Activity activity, Menu menu) {
+
+ if (activity instanceof AgendaActivity) {
+ menu.setGroupVisible(MENU_GROUP_AGENDA, false);
+ menu.setGroupEnabled(MENU_GROUP_AGENDA, false);
+ } else {
+ menu.setGroupVisible(MENU_GROUP_AGENDA, true);
+ menu.setGroupEnabled(MENU_GROUP_AGENDA, true);
+ }
+
+ if (activity instanceof DayActivity) {
+ menu.setGroupVisible(MENU_GROUP_DAY, false);
+ menu.setGroupEnabled(MENU_GROUP_DAY, false);
+ } else {
+ menu.setGroupVisible(MENU_GROUP_DAY, true);
+ menu.setGroupEnabled(MENU_GROUP_DAY, true);
+ }
+
+ if (activity instanceof WeekActivity) {
+ menu.setGroupVisible(MENU_GROUP_WEEK, false);
+ menu.setGroupEnabled(MENU_GROUP_WEEK, false);
+ } else {
+ menu.setGroupVisible(MENU_GROUP_WEEK, true);
+ menu.setGroupEnabled(MENU_GROUP_WEEK, true);
+ }
+
+ if (activity instanceof MonthActivity) {
+ menu.setGroupVisible(MENU_GROUP_MONTH, false);
+ menu.setGroupEnabled(MENU_GROUP_MONTH, false);
+ } else {
+ menu.setGroupVisible(MENU_GROUP_MONTH, true);
+ menu.setGroupEnabled(MENU_GROUP_MONTH, true);
+ }
+
+ if (activity instanceof EventInfoActivity) {
+ menu.setGroupVisible(MENU_GROUP_TODAY, false);
+ menu.setGroupEnabled(MENU_GROUP_TODAY, false);
+ } else {
+ menu.setGroupVisible(MENU_GROUP_TODAY, true);
+ menu.setGroupEnabled(MENU_GROUP_TODAY, true);
+ }
+ }
+
+ public static boolean onCreateOptionsMenu(Menu menu) {
+
+ MenuItem item;
+ item = menu.add(MENU_GROUP_AGENDA, MENU_AGENDA, 0, R.string.agenda_view);
+ item.setIcon(android.R.drawable.ic_menu_agenda);
+ item.setAlphabeticShortcut('a');
+
+ item = menu.add(MENU_GROUP_DAY, MENU_DAY, 0, R.string.day_view);
+ item.setIcon(android.R.drawable.ic_menu_day);
+ item.setAlphabeticShortcut('d');
+
+ item = menu.add(MENU_GROUP_WEEK, MENU_WEEK, 0, R.string.week_view);
+ item.setIcon(android.R.drawable.ic_menu_week);
+ item.setAlphabeticShortcut('w');
+
+ item = menu.add(MENU_GROUP_MONTH, MENU_MONTH, 0, R.string.month_view);
+ item.setIcon(android.R.drawable.ic_menu_month);
+ item.setAlphabeticShortcut('m');
+
+ item = menu.add(MENU_GROUP_EVENT_CREATE, MENU_EVENT_CREATE, 0, R.string.event_create);
+ item.setIcon(android.R.drawable.ic_menu_add);
+ item.setAlphabeticShortcut('n');
+
+ item = menu.add(MENU_GROUP_TODAY, MENU_GOTO_TODAY, 0, R.string.goto_today);
+ item.setIcon(android.R.drawable.ic_menu_today);
+ item.setAlphabeticShortcut('t');
+
+ item = menu.add(MENU_GROUP_SELECT_CALENDARS, MENU_SELECT_CALENDARS,
+ 0, R.string.menu_select_calendars);
+ item.setIcon(android.R.drawable.ic_menu_manage);
+
+ item = menu.add(MENU_GROUP_PREFERENCES, MENU_PREFERENCES, 0, R.string.menu_preferences);
+ item.setIcon(android.R.drawable.ic_menu_preferences);
+ item.setAlphabeticShortcut('p');
+
+ return true;
+ }
+
+ public static boolean onOptionsItemSelected(Activity activity, MenuItem item, Navigator nav) {
+ switch (item.getItemId()) {
+ case MENU_SELECT_CALENDARS: {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setClass(activity, SelectCalendarsActivity.class);
+ activity.startActivity(intent);
+ return true;
+ }
+ case MENU_GOTO_TODAY:
+ nav.goToToday();
+ return true;
+ case MENU_PREFERENCES:
+ switchTo(activity, CalendarPreferenceActivity.class.getName(), nav.getSelectedTime());
+ return true;
+ case MENU_AGENDA:
+ switchTo(activity, AgendaActivity.class.getName(), nav.getSelectedTime());
+ activity.finish();
+ return true;
+ case MENU_DAY:
+ switchTo(activity, DayActivity.class.getName(), nav.getSelectedTime());
+ activity.finish();
+ return true;
+ case MENU_WEEK:
+ switchTo(activity, WeekActivity.class.getName(), nav.getSelectedTime());
+ activity.finish();
+ return true;
+ case MENU_MONTH:
+ switchTo(activity, MonthActivity.class.getName(), nav.getSelectedTime());
+ activity.finish();
+ return true;
+ case MENU_EVENT_CREATE: {
+ long startMillis = nav.getSelectedTime();
+ long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
+ Intent intent = new Intent(Intent.ACTION_EDIT);
+ intent.setClassName(activity, EditEvent.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, startMillis);
+ intent.putExtra(EVENT_END_TIME, endMillis);
+ intent.putExtra(EditEvent.EVENT_ALL_DAY, nav.getAllDay());
+ activity.startActivity(intent);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /* package */ static void switchTo(Activity activity, String className, long startMillis) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setClassName(activity, className);
+ intent.putExtra(EVENT_BEGIN_TIME, startMillis);
+ activity.startActivity(intent);
+ }
+}
diff --git a/src/com/android/calendar/MonthActivity.java b/src/com/android/calendar/MonthActivity.java
new file mode 100644
index 00000000..89b07eca
--- /dev/null
+++ b/src/com/android/calendar/MonthActivity.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2006 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 static android.provider.Calendar.EVENT_BEGIN_TIME;
+import dalvik.system.VMRuntime;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.database.ContentObserver;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.provider.Calendar.Events;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Animation.AnimationListener;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.ViewSwitcher;
+import android.widget.Gallery.LayoutParams;
+
+import java.util.Calendar;
+
+public class MonthActivity extends Activity implements ViewSwitcher.ViewFactory,
+ Navigator, AnimationListener {
+ private static final int INITIAL_HEAP_SIZE = 4 * 1024 * 1024;
+ private Animation mInAnimationPast;
+ private Animation mInAnimationFuture;
+ private Animation mOutAnimationPast;
+ private Animation mOutAnimationFuture;
+ private ViewSwitcher mSwitcher;
+ private Time mTime;
+
+ private ContentResolver mContentResolver;
+ EventLoader mEventLoader;
+ private int mStartDay;
+
+ private ProgressBar mProgressBar;
+
+ protected void startProgressSpinner() {
+ // start the progress spinner
+ mProgressBar.setVisibility(View.VISIBLE);
+ }
+
+ protected void stopProgressSpinner() {
+ // stop the progress spinner
+ mProgressBar.setVisibility(View.GONE);
+ }
+
+ /* ViewSwitcher.ViewFactory interface methods */
+ public View makeView() {
+ MonthView mv = new MonthView(this, this);
+ mv.setLayoutParams(new ViewSwitcher.LayoutParams(
+ LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
+ mv.setSelectedTime(mTime);
+ return mv;
+ }
+
+ /* Navigator interface methods */
+ public void goTo(Time time) {
+ TextView title = (TextView) findViewById(R.id.title);
+ title.setText(Utils.formatMonthYear(time));
+
+ MonthView current = (MonthView) mSwitcher.getCurrentView();
+ current.dismissPopup();
+
+ Time currentTime = current.getTime();
+
+ // Compute a month number that is monotonically increasing for any
+ // two adjacent months.
+ // This is faster than calling getSelectedTime() because we avoid
+ // a call to Time#normalize().
+ int currentMonth = currentTime.month + currentTime.year * 12;
+ int nextMonth = time.month + time.year * 12;
+ if (nextMonth < currentMonth) {
+ mSwitcher.setInAnimation(mInAnimationPast);
+ mSwitcher.setOutAnimation(mOutAnimationPast);
+ } else {
+ mSwitcher.setInAnimation(mInAnimationFuture);
+ mSwitcher.setOutAnimation(mOutAnimationFuture);
+ }
+
+ MonthView next = (MonthView) mSwitcher.getNextView();
+ next.setSelectionMode(current.getSelectionMode());
+ next.setSelectedTime(time);
+ next.reloadEvents();
+ next.animationStarted();
+ mSwitcher.showNext();
+ next.requestFocus();
+ mTime = time;
+ }
+
+ public void goToToday() {
+ Time now = new Time();
+ now.set(System.currentTimeMillis());
+
+ TextView title = (TextView) findViewById(R.id.title);
+ title.setText(Utils.formatMonthYear(now));
+ mTime = now;
+
+ MonthView view = (MonthView) mSwitcher.getCurrentView();
+ view.setSelectedTime(now);
+ view.reloadEvents();
+ }
+
+ public long getSelectedTime() {
+ MonthView mv = (MonthView) mSwitcher.getCurrentView();
+ return mv.getSelectedTimeInMillis();
+ }
+
+ public boolean getAllDay() {
+ return false;
+ }
+
+ int getStartDay() {
+ return mStartDay;
+ }
+
+ void eventsChanged() {
+ MonthView view = (MonthView) mSwitcher.getCurrentView();
+ view.reloadEvents();
+ }
+
+ /**
+ * Listens for intent broadcasts
+ */
+ private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_TIME_CHANGED)
+ || action.equals(Intent.ACTION_DATE_CHANGED)
+ || action.equals(Intent.ACTION_TIMEZONE_CHANGED)) {
+ eventsChanged();
+ }
+ }
+ };
+
+ // Create an observer so that we can update the views whenever a
+ // Calendar event changes.
+ private ContentObserver mObserver = new ContentObserver(new Handler())
+ {
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ eventsChanged();
+ }
+ };
+
+ public void onAnimationStart(Animation animation) {
+ }
+
+ // Notifies the MonthView when an animation has finished.
+ public void onAnimationEnd(Animation animation) {
+ MonthView monthView = (MonthView) mSwitcher.getCurrentView();
+ monthView.animationFinished();
+ }
+
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ // Eliminate extra GCs during startup by setting the initial heap size to 4MB.
+ // TODO: We should restore the old heap size once the activity reaches the idle state
+ long oldHeapSize = VMRuntime.getRuntime().setMinimumHeapSize(INITIAL_HEAP_SIZE);
+
+ setContentView(R.layout.month_activity);
+ mContentResolver = getContentResolver();
+
+ long time;
+ if (icicle != null) {
+ time = icicle.getLong(EVENT_BEGIN_TIME);
+ } else {
+ time = Utils.timeFromIntentInMillis(getIntent());
+ }
+
+ mTime = new Time();
+ mTime.set(time);
+ mTime.normalize(true);
+
+ // Get first day of week based on locale and populate the day headers
+ mStartDay = Calendar.getInstance().getFirstDayOfWeek();
+ int diff = mStartDay - Calendar.SUNDAY - 1;
+
+ String dayString = DateUtils.getDayOfWeekString((Calendar.SUNDAY + diff) % 7 + 1,
+ DateUtils.LENGTH_MEDIUM);
+ ((TextView) findViewById(R.id.day0)).setText(dayString);
+ dayString = DateUtils.getDayOfWeekString((Calendar.MONDAY + diff) % 7 + 1,
+ DateUtils.LENGTH_MEDIUM);
+ ((TextView) findViewById(R.id.day1)).setText(dayString);
+ dayString = DateUtils.getDayOfWeekString((Calendar.TUESDAY + diff) % 7 + 1,
+ DateUtils.LENGTH_MEDIUM);
+ ((TextView) findViewById(R.id.day2)).setText(dayString);
+ dayString = DateUtils.getDayOfWeekString((Calendar.WEDNESDAY + diff) % 7 + 1,
+ DateUtils.LENGTH_MEDIUM);
+ ((TextView) findViewById(R.id.day3)).setText(dayString);
+ dayString = DateUtils.getDayOfWeekString((Calendar.THURSDAY + diff) % 7 + 1,
+ DateUtils.LENGTH_MEDIUM);
+ ((TextView) findViewById(R.id.day4)).setText(dayString);
+ dayString = DateUtils.getDayOfWeekString((Calendar.FRIDAY + diff) % 7 + 1,
+ DateUtils.LENGTH_MEDIUM);
+ ((TextView) findViewById(R.id.day5)).setText(dayString);
+ dayString = DateUtils.getDayOfWeekString((Calendar.SATURDAY + diff) % 7 + 1,
+ DateUtils.LENGTH_MEDIUM);
+ ((TextView) findViewById(R.id.day6)).setText(dayString);
+
+ // Set the initial title
+ TextView title = (TextView) findViewById(R.id.title);
+ title.setText(Utils.formatMonthYear(mTime));
+
+ mEventLoader = new EventLoader(this);
+ mProgressBar = (ProgressBar) findViewById(R.id.progress_circular);
+
+ mSwitcher = (ViewSwitcher) findViewById(R.id.switcher);
+ mSwitcher.setFactory(this);
+ mSwitcher.getCurrentView().requestFocus();
+
+ mInAnimationPast = AnimationUtils.loadAnimation(this, R.anim.slide_down_in);
+ mOutAnimationPast = AnimationUtils.loadAnimation(this, R.anim.slide_down_out);
+ mInAnimationFuture = AnimationUtils.loadAnimation(this, R.anim.slide_up_in);
+ mOutAnimationFuture = AnimationUtils.loadAnimation(this, R.anim.slide_up_out);
+
+ mInAnimationPast.setAnimationListener(this);
+ mInAnimationFuture.setAnimationListener(this);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (isFinishing()) {
+ mEventLoader.stopBackgroundThread();
+ }
+ mContentResolver.unregisterContentObserver(mObserver);
+ unregisterReceiver(mIntentReceiver);
+
+ MonthView view = (MonthView) mSwitcher.getCurrentView();
+ view.dismissPopup();
+ view = (MonthView) mSwitcher.getNextView();
+ view.dismissPopup();
+ mEventLoader.stopBackgroundThread();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mEventLoader.startBackgroundThread();
+ eventsChanged();
+
+ MonthView view1 = (MonthView) mSwitcher.getCurrentView();
+ MonthView view2 = (MonthView) mSwitcher.getNextView();
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ String str = prefs.getString(CalendarPreferenceActivity.KEY_DETAILED_VIEW,
+ CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW);
+ view1.setDetailedView(str);
+ view2.setDetailedView(str);
+
+ // Record Month View as the (new) start view
+ String activityString = CalendarApplication.ACTIVITY_NAMES[CalendarApplication.MONTH_VIEW_ID];
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(CalendarPreferenceActivity.KEY_START_VIEW, activityString);
+ editor.commit();
+
+ // Register for Intent broadcasts
+ IntentFilter filter = new IntentFilter();
+
+ filter.addAction(Intent.ACTION_TIME_CHANGED);
+ filter.addAction(Intent.ACTION_DATE_CHANGED);
+ filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+ registerReceiver(mIntentReceiver, filter);
+
+ mContentResolver.registerContentObserver(Events.CONTENT_URI,
+ true, mObserver);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putLong(EVENT_BEGIN_TIME, mTime.toMillis(true));
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ finish();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ MenuHelper.onPrepareOptionsMenu(this, menu);
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuHelper.onCreateOptionsMenu(menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ MenuHelper.onOptionsItemSelected(this, item, this);
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/com/android/calendar/MonthView.java b/src/com/android/calendar/MonthView.java
new file mode 100644
index 00000000..f6db3b80
--- /dev/null
+++ b/src/com/android/calendar/MonthView.java
@@ -0,0 +1,1366 @@
+/*
+ * Copyright (C) 2006 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 static android.provider.Calendar.EVENT_BEGIN_TIME;
+import static android.provider.Calendar.EVENT_END_TIME;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.provider.Calendar.BusyBits;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.util.DayOfMonthCursor;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.ContextMenu;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+
+public class MonthView extends View implements View.OnCreateContextMenuListener {
+
+ private static final boolean PROFILE_LOAD_TIME = false;
+ private static final boolean DEBUG_BUSYBITS = false;
+
+ private static final int WEEK_GAP = 0;
+ private static final int MONTH_DAY_GAP = 1;
+ private static final float HOUR_GAP = 0.5f;
+
+ private static final int MONTH_DAY_TEXT_SIZE = 20;
+ private static final int WEEK_BANNER_HEIGHT = 17;
+ private static final int WEEK_TEXT_SIZE = 15;
+ private static final int WEEK_TEXT_PADDING = 3;
+ private static final int BUSYBIT_WIDTH = 10;
+ private static final int BUSYBIT_RIGHT_MARGIN = 3;
+ private static final int BUSYBIT_TOP_BOTTOM_MARGIN = 7;
+
+ private static final int HORIZONTAL_FLING_THRESHOLD = 50;
+
+ private int mCellHeight;
+ private int mBorder;
+ private boolean mLaunchDayView;
+
+ private GestureDetector mGestureDetector;
+
+ private String mDetailedView = CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW;
+
+ private Time mToday;
+ private Time mViewCalendar;
+ private Time mSavedTime = new Time(); // the time when we entered this view
+
+ // This Time object is used to set the time for the other Month view.
+ private Time mOtherViewCalendar = new Time();
+
+ // This Time object is used for temporary calculations and is allocated
+ // once to avoid extra garbage collection
+ private Time mTempTime = new Time();
+
+ private DayOfMonthCursor mCursor;
+
+ private Drawable mBoxSelected;
+ private Drawable mBoxPressed;
+ private Drawable mBoxLongPressed;
+ private Drawable mDnaEmpty;
+ private Drawable mDnaTop;
+ private Drawable mDnaMiddle;
+ private Drawable mDnaBottom;
+ private int mCellWidth;
+
+ private Resources mResources;
+ private MonthActivity mParentActivity;
+ private Navigator mNavigator;
+ private final EventGeometry mEventGeometry;
+
+ // Pre-allocate and reuse
+ private Rect mRect = new Rect();
+
+ // The number of hours represented by one busy bit
+ private static final int HOURS_PER_BUSY_SLOT = 4;
+
+ // The number of database intervals represented by one busy bit (slot)
+ private static final int INTERVALS_PER_BUSY_SLOT = 4 * 60 / BusyBits.MINUTES_PER_BUSY_INTERVAL;
+
+ // The bit mask for coalescing the raw busy bits from the database
+ // (1 bit per hour) into the busy bits per slot (4-hour slots).
+ private static final int BUSY_SLOT_MASK = (1 << INTERVALS_PER_BUSY_SLOT) - 1;
+
+ // The number of slots in a day
+ private static final int SLOTS_PER_DAY = 24 / HOURS_PER_BUSY_SLOT;
+
+ // There is one "busy" bit for each slot of time.
+ private byte[][] mBusyBits = new byte[31][SLOTS_PER_DAY];
+
+ // Raw busy bits from database
+ private int[] mRawBusyBits = new int[31];
+ private int[] mAllDayCounts = new int[31];
+
+ private PopupWindow mPopup;
+ private View mPopupView;
+ private static final int POPUP_HEIGHT = 100;
+ private int mPreviousPopupHeight;
+ private static final int POPUP_DISMISS_DELAY = 3000;
+ private DismissPopup mDismissPopup = new DismissPopup();
+
+ // For drawing to an off-screen Canvas
+ private Bitmap mBitmap;
+ private Canvas mCanvas;
+ private boolean mRedrawScreen = true;
+ private Rect mBitmapRect = new Rect();
+ private boolean mAnimating;
+
+ // These booleans disable features that were taken out of the spec.
+ private boolean mShowWeekNumbers = false;
+ private boolean mShowToast = false;
+
+ // Bitmap caches.
+ // These improve performance by minimizing calls to NinePatchDrawable.draw() for common
+ // drawables for events and day backgrounds.
+ // mEventBitmapCache is indexed by an integer constructed from the bits in the busyBits
+ // field. It is not expected to be larger than 12 bits (if so, we should switch to using a Map).
+ // mDayBitmapCache is indexed by a unique integer constructed from the width/height.
+ private SparseArray<Bitmap> mEventBitmapCache = new SparseArray<Bitmap>(1<<SLOTS_PER_DAY);
+ private SparseArray<Bitmap> mDayBitmapCache = new SparseArray<Bitmap>(4);
+
+ private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler();
+
+ /**
+ * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS.
+ */
+ private static final int SELECTION_HIDDEN = 0;
+ private static final int SELECTION_PRESSED = 1;
+ private static final int SELECTION_SELECTED = 2;
+ private static final int SELECTION_LONGPRESS = 3;
+
+ // Modulo used to pack (width,height) into a unique integer
+ private static final int MODULO_SHIFT = 16;
+
+ private int mSelectionMode = SELECTION_HIDDEN;
+
+ /**
+ * The first Julian day of the current month.
+ */
+ private int mFirstJulianDay;
+
+ private final EventLoader mEventLoader;
+
+ private ArrayList<Event> mEvents = new ArrayList<Event>();
+
+ private Drawable mTodayBackground;
+ private Drawable mDayBackground;
+
+ // Cached colors
+ private int mMonthOtherMonthColor;
+ private int mMonthWeekBannerColor;
+ private int mMonthOtherMonthBannerColor;
+ private int mMonthOtherMonthDayNumberColor;
+ private int mMonthDayNumberColor;
+ private int mMonthTodayNumberColor;
+
+ public MonthView(MonthActivity activity, Navigator navigator) {
+ super(activity);
+ mEventLoader = activity.mEventLoader;
+ mNavigator = navigator;
+ mEventGeometry = new EventGeometry();
+ mEventGeometry.setMinEventHeight(1.0f);
+ mEventGeometry.setHourGap(HOUR_GAP);
+ init(activity);
+ }
+
+ private void init(MonthActivity activity) {
+ setFocusable(true);
+ setClickable(true);
+ setOnCreateContextMenuListener(this);
+ mParentActivity = activity;
+ mViewCalendar = new Time();
+ long now = System.currentTimeMillis();
+ mViewCalendar.set(now);
+ mViewCalendar.monthDay = 1;
+ long millis = mViewCalendar.normalize(true /* ignore DST */);
+ mFirstJulianDay = Time.getJulianDay(millis, mViewCalendar.gmtoff);
+ mViewCalendar.set(now);
+
+ mCursor = new DayOfMonthCursor(mViewCalendar.year, mViewCalendar.month,
+ mViewCalendar.monthDay, mParentActivity.getStartDay());
+ mToday = new Time();
+ mToday.set(System.currentTimeMillis());
+
+ mResources = activity.getResources();
+ mBoxSelected = mResources.getDrawable(R.drawable.month_view_selected);
+ mBoxPressed = mResources.getDrawable(R.drawable.month_view_pressed);
+ mBoxLongPressed = mResources.getDrawable(R.drawable.month_view_longpress);
+
+ mDnaEmpty = mResources.getDrawable(R.drawable.dna_empty);
+ mDnaTop = mResources.getDrawable(R.drawable.dna_1_of_6);
+ mDnaMiddle = mResources.getDrawable(R.drawable.dna_2345_of_6);
+ mDnaBottom = mResources.getDrawable(R.drawable.dna_6_of_6);
+ mTodayBackground = mResources.getDrawable(R.drawable.month_view_today_background);
+ mDayBackground = mResources.getDrawable(R.drawable.month_view_background);
+
+ // Cache color lookups
+ Resources res = getResources();
+ mMonthOtherMonthColor = res.getColor(R.color.month_other_month);
+ mMonthWeekBannerColor = res.getColor(R.color.month_week_banner);
+ mMonthOtherMonthBannerColor = res.getColor(R.color.month_other_month_banner);
+ mMonthOtherMonthDayNumberColor = res.getColor(R.color.month_other_month_day_number);
+ mMonthDayNumberColor = res.getColor(R.color.month_day_number);
+ mMonthTodayNumberColor = res.getColor(R.color.month_today_number);
+
+ if (mShowToast) {
+ LayoutInflater inflater;
+ inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mPopupView = inflater.inflate(R.layout.month_bubble, null);
+ mPopup = new PopupWindow(activity);
+ mPopup.setContentView(mPopupView);
+ Resources.Theme dialogTheme = getResources().newTheme();
+ dialogTheme.applyStyle(android.R.style.Theme_Dialog, true);
+ TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] {
+ android.R.attr.windowBackground });
+ mPopup.setBackgroundDrawable(ta.getDrawable(0));
+ ta.recycle();
+ }
+
+ mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ // The user might do a slow "fling" after touching the screen
+ // and we don't want the long-press to pop up a context menu.
+ // Setting mLaunchDayView to false prevents the long-press.
+ mLaunchDayView = false;
+ mSelectionMode = SELECTION_HIDDEN;
+
+ int distanceX = Math.abs((int) e2.getX() - (int) e1.getX());
+ int distanceY = Math.abs((int) e2.getY() - (int) e1.getY());
+ if (distanceY < HORIZONTAL_FLING_THRESHOLD || distanceY < distanceX) {
+ return false;
+ }
+
+ // Switch to a different month
+ Time time = mOtherViewCalendar;
+ time.set(mViewCalendar);
+ if (velocityY < 0) {
+ time.month += 1;
+ } else {
+ time.month -= 1;
+ }
+ time.normalize(true);
+ mParentActivity.goTo(time);
+
+ return true;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ mLaunchDayView = false;
+ return true;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ int x = (int) e.getX();
+ int y = (int) e.getY();
+ int row = (y - WEEK_GAP) / (WEEK_GAP + mCellHeight);
+ int col = (x - mBorder) / (MONTH_DAY_GAP + mCellWidth);
+ if (row > 5) {
+ row = 5;
+ }
+ if (col > 6) {
+ col = 6;
+ }
+
+ // Launch the Day/Agenda view when the finger lifts up,
+ // unless the finger moves before lifting up.
+ mLaunchDayView = true;
+
+ // Highlight the selected day.
+ mCursor.setSelectedRowColumn(row, col);
+ mSelectionMode = SELECTION_PRESSED;
+ mRedrawScreen = true;
+ invalidate();
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ // If mLaunchDayView is true, then we haven't done any scrolling
+ // after touching the screen, so allow long-press to proceed
+ // with popping up the context menu.
+ if (mLaunchDayView) {
+ mLaunchDayView = false;
+ mSelectionMode = SELECTION_LONGPRESS;
+ mRedrawScreen = true;
+ invalidate();
+ performLongClick();
+ }
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2,
+ float distanceX, float distanceY) {
+ // If the user moves his finger after touching, then do not
+ // launch the Day view when he lifts his finger. Also, turn
+ // off the selection.
+ mLaunchDayView = false;
+
+ if (mSelectionMode != SELECTION_HIDDEN) {
+ mSelectionMode = SELECTION_HIDDEN;
+ mRedrawScreen = true;
+ invalidate();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ if (mLaunchDayView) {
+ mSelectionMode = SELECTION_SELECTED;
+ mRedrawScreen = true;
+ invalidate();
+ mLaunchDayView = false;
+ int x = (int) e.getX();
+ int y = (int) e.getY();
+ long millis = getSelectedMillisFor(x, y);
+ Utils.startActivity(getContext(), mDetailedView, millis);
+ mParentActivity.finish();
+ }
+
+ return true;
+ }
+ });
+ }
+
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
+ MenuItem item;
+
+ final long startMillis = getSelectedTimeInMillis();
+ final int flags = DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_ABBREV_MONTH;
+
+ final String title = DateUtils.formatDateTime(mParentActivity, startMillis, flags);
+ menu.setHeaderTitle(title);
+
+ item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_day);
+ item.setAlphabeticShortcut('d');
+
+ item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_agenda);
+ item.setAlphabeticShortcut('a');
+
+ item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_add);
+ item.setAlphabeticShortcut('n');
+ }
+
+ private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener {
+ public boolean onMenuItemClick(MenuItem item) {
+ switch (item.getItemId()) {
+ case MenuHelper.MENU_DAY: {
+ long startMillis = getSelectedTimeInMillis();
+ MenuHelper.switchTo(mParentActivity, DayActivity.class.getName(), startMillis);
+ mParentActivity.finish();
+ break;
+ }
+ case MenuHelper.MENU_AGENDA: {
+ long startMillis = getSelectedTimeInMillis();
+ MenuHelper.switchTo(mParentActivity, AgendaActivity.class.getName(), startMillis);
+ mParentActivity.finish();
+ break;
+ }
+ case MenuHelper.MENU_EVENT_CREATE: {
+ long startMillis = getSelectedTimeInMillis();
+ long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setClassName(mContext, EditEvent.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, startMillis);
+ intent.putExtra(EVENT_END_TIME, endMillis);
+ mParentActivity.startActivity(intent);
+ break;
+ }
+ default: {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ void reloadEvents() {
+ // Get the date for the beginning of the month
+ Time monthStart = mTempTime;
+ monthStart.set(mViewCalendar);
+ monthStart.monthDay = 1;
+ monthStart.hour = 0;
+ monthStart.minute = 0;
+ monthStart.second = 0;
+ long millis = monthStart.normalize(true /* ignore isDst */);
+ int startDay = Time.getJulianDay(millis, monthStart.gmtoff);
+
+ // Load the busy-bits in the background
+ mParentActivity.startProgressSpinner();
+ final long startMillis;
+ if (PROFILE_LOAD_TIME) {
+ startMillis = SystemClock.uptimeMillis();
+ } else {
+ // To avoid a compiler error that this variable might not be initialized.
+ startMillis = 0;
+ }
+ mEventLoader.loadBusyBitsInBackground(startDay, 31, mRawBusyBits, mAllDayCounts,
+ new Runnable() {
+ public void run() {
+ convertBusyBits();
+ if (PROFILE_LOAD_TIME) {
+ long endMillis = SystemClock.uptimeMillis();
+ long elapsed = endMillis - startMillis;
+ Log.i("Cal", (mViewCalendar.month+1) + "/" + mViewCalendar.year + " Month view load busybits: " + elapsed);
+ }
+ mRedrawScreen = true;
+ mParentActivity.stopProgressSpinner();
+ invalidate();
+ }
+ });
+ }
+
+ void animationStarted() {
+ mAnimating = true;
+ }
+
+ void animationFinished() {
+ mAnimating = false;
+ mRedrawScreen = true;
+ invalidate();
+ }
+
+ @Override
+ protected void onSizeChanged(int width, int height, int oldw, int oldh) {
+ drawingCalc(width, height);
+ // If the size changed, then we should rebuild the bitmaps...
+ clearBitmapCache();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ // No need to hang onto the bitmaps...
+ clearBitmapCache();
+ if (mBitmap != null) {
+ mBitmap.recycle();
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mRedrawScreen) {
+ if (mCanvas == null) {
+ drawingCalc(getWidth(), getHeight());
+ }
+
+ // If we are zero-sized, the canvas will remain null so check again
+ if (mCanvas != null) {
+ // Clear the background
+ final Canvas bitmapCanvas = mCanvas;
+ bitmapCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ doDraw(bitmapCanvas);
+ mRedrawScreen = false;
+ }
+ }
+
+ // If we are zero-sized, the bitmap will be null so guard against this
+ if (mBitmap != null) {
+ canvas.drawBitmap(mBitmap, mBitmapRect, mBitmapRect, null);
+ }
+ }
+
+ private void doDraw(Canvas canvas) {
+ boolean isLandscape = getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_LANDSCAPE;
+
+ Paint p = new Paint();
+ Rect r = mRect;
+ int columnDay1 = mCursor.getColumnOf(1);
+
+ // Get the Julian day for the date at row 0, column 0.
+ int day = mFirstJulianDay - columnDay1;
+
+ int weekNum = 0;
+ Calendar calendar = null;
+ if (mShowWeekNumbers) {
+ calendar = Calendar.getInstance();
+ boolean noPrevMonth = (columnDay1 == 0);
+
+ // Compute the week number for the first row.
+ weekNum = getWeekOfYear(0, 0, noPrevMonth, calendar);
+ }
+
+ for (int row = 0; row < 6; row++) {
+ for (int column = 0; column < 7; column++) {
+ drawBox(day, weekNum, row, column, canvas, p, r, isLandscape);
+ day += 1;
+ }
+
+ if (mShowWeekNumbers) {
+ weekNum += 1;
+ if (weekNum >= 53) {
+ boolean inCurrentMonth = (day - mFirstJulianDay < 31);
+ weekNum = getWeekOfYear(row + 1, 0, inCurrentMonth, calendar);
+ }
+ }
+ }
+
+ drawGrid(canvas, p);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mGestureDetector.onTouchEvent(event)) {
+ return true;
+ }
+
+ return super.onTouchEvent(event);
+ }
+
+ private long getSelectedMillisFor(int x, int y) {
+ int row = (y - WEEK_GAP) / (WEEK_GAP + mCellHeight);
+ int column = (x - mBorder) / (MONTH_DAY_GAP + mCellWidth);
+ if (column > 6) {
+ column = 6;
+ }
+
+ DayOfMonthCursor c = mCursor;
+ Time time = mTempTime;
+ time.set(mViewCalendar);
+
+ // Compute the day number from the row and column. If the row and
+ // column are in a different month from the current one, then the
+ // monthDay might be negative or it might be greater than the number
+ // of days in this month, but that is okay because the normalize()
+ // method will adjust the month (and year) if necessary.
+ time.monthDay = 7 * row + column - c.getOffset() + 1;
+ return time.normalize(true);
+ }
+
+ /**
+ * Create a bitmap at the origin and draw the drawable to it using the bounds specified by rect.
+ *
+ * @param drawable the drawable we wish to render
+ * @param width the width of the resulting bitmap
+ * @param height the height of the resulting bitmap
+ * @return a new bitmap
+ */
+ private Bitmap createBitmap(Drawable drawable, int width, int height) {
+ // Create a bitmap with the same format as mBitmap (should be Bitmap.Config.ARGB_8888)
+ Bitmap bitmap = Bitmap.createBitmap(width, height, mBitmap.getConfig());
+
+ // Draw the drawable into the bitmap at the origin.
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, width, height);
+ drawable.draw(canvas);
+ return bitmap;
+ }
+
+ /**
+ * Clears the bitmap cache. Generally only needed when the screen size changed.
+ */
+ private void clearBitmapCache() {
+ recycleAndClearBitmapCache(mEventBitmapCache);
+ recycleAndClearBitmapCache(mDayBitmapCache);
+ }
+
+ private void recycleAndClearBitmapCache(SparseArray<Bitmap> bitmapCache) {
+ int size = bitmapCache.size();
+ for(int i = 0; i < size; i++) {
+ bitmapCache.valueAt(i).recycle();
+ }
+ bitmapCache.clear();
+
+ }
+
+ /**
+ * Draw the grid lines for the calendar
+ * @param canvas The canvas to draw on.
+ * @param p The paint used for drawing.
+ */
+ private void drawGrid(Canvas canvas, Paint p) {
+ p.setColor(mMonthOtherMonthColor);
+ p.setAntiAlias(false);
+
+ final int width = getMeasuredWidth();
+ final int height = getMeasuredHeight();
+
+ for (int row = 0; row < 6; row++) {
+ int y = WEEK_GAP + row * (WEEK_GAP + mCellHeight) - 1;
+ canvas.drawLine(0, y, width, y, p);
+ }
+ for (int column = 1; column < 7; column++) {
+ int x = mBorder + column * (MONTH_DAY_GAP + mCellWidth) - 1;
+ canvas.drawLine(x, WEEK_GAP, x, height, p);
+ }
+ }
+
+ /**
+ * Draw a single box onto the canvas.
+ * @param day The Julian day.
+ * @param weekNum The week number.
+ * @param row The row of the box (0-5).
+ * @param column The column of the box (0-6).
+ * @param canvas The canvas to draw on.
+ * @param p The paint used for drawing.
+ * @param r The rectangle used for each box.
+ * @param isLandscape Is the current orientation landscape.
+ */
+ private void drawBox(int day, int weekNum, int row, int column, Canvas canvas, Paint p,
+ Rect r, boolean isLandscape) {
+
+ // Only draw the selection if we are in the press state or if we have
+ // moved the cursor with key input.
+ boolean drawSelection = false;
+ if (mSelectionMode != SELECTION_HIDDEN) {
+ drawSelection = mCursor.isSelected(row, column);
+ }
+
+ boolean withinCurrentMonth = mCursor.isWithinCurrentMonth(row, column);
+ boolean isToday = false;
+ int dayOfBox = mCursor.getDayAt(row, column);
+ if (dayOfBox == mToday.monthDay && mCursor.getYear() == mToday.year
+ && mCursor.getMonth() == mToday.month) {
+ isToday = true;
+ }
+
+ int y = WEEK_GAP + row*(WEEK_GAP + mCellHeight);
+ int x = mBorder + column*(MONTH_DAY_GAP + mCellWidth);
+
+ r.left = x;
+ r.top = y;
+ r.right = x + mCellWidth;
+ r.bottom = y + mCellHeight;
+
+
+ // Adjust the left column, right column, and bottom row to leave
+ // no border.
+ if (column == 0) {
+ r.left = -1;
+ } else if (column == 6) {
+ r.right += mBorder + 2;
+ }
+
+ if (row == 5) {
+ r.bottom = getMeasuredHeight();
+ }
+
+ // Draw the cell contents (excluding monthDay number)
+ if (!withinCurrentMonth) {
+ boolean firstDayOfNextmonth = isFirstDayOfNextMonth(row, column);
+
+ // Adjust cell boundaries to compensate for the different border
+ // style.
+ r.top--;
+ if (column != 0) {
+ r.left--;
+ }
+ } else if (drawSelection) {
+ if (mSelectionMode == SELECTION_SELECTED) {
+ mBoxSelected.setBounds(r);
+ mBoxSelected.draw(canvas);
+ } else if (mSelectionMode == SELECTION_PRESSED) {
+ mBoxPressed.setBounds(r);
+ mBoxPressed.draw(canvas);
+ } else {
+ mBoxLongPressed.setBounds(r);
+ mBoxLongPressed.draw(canvas);
+ }
+
+ drawEvents(day, canvas, r, p);
+ if (!mAnimating) {
+ updateEventDetails(day);
+ }
+ } else {
+ // Today gets a different background
+ if (isToday) {
+ // We could cache this for a little bit more performance, but it's not on the
+ // performance radar...
+ Drawable background = mTodayBackground;
+ background.setBounds(r);
+ background.draw(canvas);
+ } else {
+ // Use the bitmap cache to draw the day background
+ int width = r.right - r.left;
+ int height = r.bottom - r.top;
+ // Compute a unique id that depends on width and height.
+ int id = (height << MODULO_SHIFT) | width;
+ Bitmap bitmap = mDayBitmapCache.get(id);
+ if (bitmap == null) {
+ bitmap = createBitmap(mDayBackground, width, height);
+ mDayBitmapCache.put(id, bitmap);
+ }
+ canvas.drawBitmap(bitmap, r.left, r.top, p);
+ }
+ drawEvents(day, canvas, r, p);
+ }
+
+ // Draw week number
+ if (mShowWeekNumbers && column == 0) {
+ // Draw the banner
+ p.setStyle(Paint.Style.FILL);
+ p.setColor(mMonthWeekBannerColor);
+ int right = r.right;
+ r.right = right - BUSYBIT_WIDTH - BUSYBIT_RIGHT_MARGIN;
+ if (isLandscape) {
+ int bottom = r.bottom;
+ r.bottom = r.top + WEEK_BANNER_HEIGHT;
+ r.left++;
+ canvas.drawRect(r, p);
+ r.bottom = bottom;
+ r.left--;
+ } else {
+ int top = r.top;
+ r.top = r.bottom - WEEK_BANNER_HEIGHT;
+ r.left++;
+ canvas.drawRect(r, p);
+ r.top = top;
+ r.left--;
+ }
+ r.right = right;
+
+ // Draw the number
+ p.setColor(mMonthOtherMonthBannerColor);
+ p.setAntiAlias(true);
+ p.setTypeface(null);
+ p.setTextSize(WEEK_TEXT_SIZE);
+ p.setTextAlign(Paint.Align.LEFT);
+
+ int textX = r.left + WEEK_TEXT_PADDING;
+ int textY;
+ if (isLandscape) {
+ textY = r.top + WEEK_BANNER_HEIGHT - WEEK_TEXT_PADDING;
+ } else {
+ textY = r.bottom - WEEK_TEXT_PADDING;
+ }
+
+ canvas.drawText(String.valueOf(weekNum), textX, textY, p);
+ }
+
+ // Draw the monthDay number
+ p.setStyle(Paint.Style.FILL);
+ p.setAntiAlias(true);
+ p.setTypeface(null);
+ p.setTextSize(MONTH_DAY_TEXT_SIZE);
+
+ if (!withinCurrentMonth) {
+ p.setColor(mMonthOtherMonthDayNumberColor);
+ } else if (drawSelection || !isToday) {
+ p.setColor(mMonthDayNumberColor);
+ } else {
+ p.setColor(mMonthTodayNumberColor);
+ }
+
+ p.setTextAlign(Paint.Align.CENTER);
+ int right = r.right - BUSYBIT_WIDTH - BUSYBIT_RIGHT_MARGIN;
+ int textX = r.left + (right - r.left) / 2; // center of text
+ int textY = r.bottom - BUSYBIT_TOP_BOTTOM_MARGIN - 2; // bottom of text
+ canvas.drawText(String.valueOf(mCursor.getDayAt(row, column)), textX, textY, p);
+ }
+
+ /**
+ * Converts the busy bits from the database that use 1-hour intervals to
+ * the 4-hour time slots needed in this view. Also, we map all-day
+ * events to the first two 4-hour time slots (that is, an all-day event
+ * will look like the first 8 hours from 12am to 8am are busy). This
+ * looks better than setting just the first 4-hour time slot because that
+ * is barely visible in landscape mode.
+ */
+ private void convertBusyBits() {
+ if (DEBUG_BUSYBITS) {
+ Log.i("Cal", "convertBusyBits() SLOTS_PER_DAY: " + SLOTS_PER_DAY
+ + " BUSY_SLOT_MASK: " + BUSY_SLOT_MASK
+ + " INTERVALS_PER_BUSY_SLOT: " + INTERVALS_PER_BUSY_SLOT);
+ for (int day = 0; day < 31; day++) {
+ int bits = mRawBusyBits[day];
+ String bitString = String.format("0x%06x", bits);
+ String valString = "";
+ for (int slot = 0; slot < SLOTS_PER_DAY; slot++) {
+ int val = bits & BUSY_SLOT_MASK;
+ bits = bits >>> INTERVALS_PER_BUSY_SLOT;
+ valString += " " + val;
+ }
+ Log.i("Cal", "[" + day + "] " + bitString + " " + valString
+ + " allday: " + mAllDayCounts[day]);
+ }
+ }
+ for (int day = 0; day < 31; day++) {
+ int bits = mRawBusyBits[day];
+ for (int slot = 0; slot < SLOTS_PER_DAY; slot++) {
+ int val = bits & BUSY_SLOT_MASK;
+ bits = bits >>> INTERVALS_PER_BUSY_SLOT;
+ if (val == 0) {
+ mBusyBits[day][slot] = 0;
+ } else {
+ mBusyBits[day][slot] = 1;
+ }
+ }
+ if (mAllDayCounts[day] > 0) {
+ mBusyBits[day][0] = 1;
+ mBusyBits[day][1] = 1;
+ }
+ }
+ }
+
+ /**
+ * Create a bitmap at the origin for the given set of busyBits.
+ *
+ * @param busyBits an array of bits with elements set to 1 if we have an event for that slot
+ * @param rect the size of the resulting
+ * @return a new bitmap
+ */
+ private Bitmap createEventBitmap(byte[] busyBits, Rect rect) {
+ // Compute the size of the smallest bitmap, excluding margins.
+ final int left = 0;
+ final int right = BUSYBIT_WIDTH;
+ final int top = 0;
+ final int bottom = (rect.bottom - rect.top) - 2 * BUSYBIT_TOP_BOTTOM_MARGIN;
+ final int height = bottom - top;
+ final int width = right - left;
+
+ final Drawable dnaEmpty = mDnaEmpty;
+ final Drawable dnaTop = mDnaTop;
+ final Drawable dnaMiddle = mDnaMiddle;
+ final Drawable dnaBottom = mDnaBottom;
+ final float slotHeight = (float) height / SLOTS_PER_DAY;
+
+ // Create a bitmap with the same format as mBitmap (should be Bitmap.Config.ARGB_8888)
+ Bitmap bitmap = Bitmap.createBitmap(width, height, mBitmap.getConfig());
+
+ // Create a canvas for drawing and draw background (dnaEmpty)
+ Canvas canvas = new Canvas(bitmap);
+ dnaEmpty.setBounds(left, top, right, bottom);
+ dnaEmpty.draw(canvas);
+
+ // The first busy bit is a drawable that is round at the top
+ if (busyBits[0] == 1) {
+ float rectBottom = top + slotHeight;
+ dnaTop.setBounds(left, top, right, (int) rectBottom);
+ dnaTop.draw(canvas);
+ }
+
+ // The last busy bit is a drawable that is round on the bottom
+ int lastIndex = busyBits.length - 1;
+ if (busyBits[lastIndex] == 1) {
+ float rectTop = bottom - slotHeight;
+ dnaBottom.setBounds(left, (int) rectTop, right, bottom);
+ dnaBottom.draw(canvas);
+ }
+
+ // Draw all intermediate pieces. We could further optimize this to
+ // draw runs of bits, but it probably won't yield much more performance.
+ float rectTop = top + slotHeight;
+ for (int index = 1; index < lastIndex; index++) {
+ float rectBottom = rectTop + slotHeight;
+ if (busyBits[index] == 1) {
+ dnaMiddle.setBounds(left, (int) rectTop, right, (int) rectBottom);
+ dnaMiddle.draw(canvas);
+ }
+ rectTop = rectBottom;
+ }
+ return bitmap;
+ }
+
+ private void drawEvents(int date, Canvas canvas, Rect rect, Paint p) {
+ // These are the coordinates of the upper left corner where we'll draw the event bitmap
+ int top = rect.top + BUSYBIT_TOP_BOTTOM_MARGIN;
+ int right = rect.right - BUSYBIT_RIGHT_MARGIN;
+ int left = right - BUSYBIT_WIDTH;
+
+ // Display the busy bits. Draw a rectangle for each run of 1-bits.
+ int day = date - mFirstJulianDay;
+ byte[] busyBits = mBusyBits[day];
+ int lastIndex = busyBits.length - 1;
+
+ // Cache index is simply all of the bits combined into an integer
+ int cacheIndex = 0;
+ for (int i = 0 ; i <= lastIndex; i++) cacheIndex |= busyBits[i] << i;
+ Bitmap bitmap = mEventBitmapCache.get(cacheIndex);
+ if (bitmap == null) {
+ // Create a bitmap that we'll reuse for all events with the same
+ // combination of busyBits.
+ bitmap = createEventBitmap(busyBits, rect);
+ mEventBitmapCache.put(cacheIndex, bitmap);
+ }
+ canvas.drawBitmap(bitmap, left, top, p);
+ }
+
+ private boolean isFirstDayOfNextMonth(int row, int column) {
+ if (column == 0) {
+ column = 6;
+ row--;
+ } else {
+ column--;
+ }
+ return mCursor.isWithinCurrentMonth(row, column);
+ }
+
+ private int getWeekOfYear(int row, int column, boolean isWithinCurrentMonth,
+ Calendar calendar) {
+ calendar.set(Calendar.DAY_OF_MONTH, mCursor.getDayAt(row, column));
+ if (isWithinCurrentMonth) {
+ calendar.set(Calendar.MONTH, mCursor.getMonth());
+ calendar.set(Calendar.YEAR, mCursor.getYear());
+ } else {
+ int month = mCursor.getMonth();
+ int year = mCursor.getYear();
+ if (row < 2) {
+ // Previous month
+ if (month == 0) {
+ year--;
+ month = 11;
+ } else {
+ month--;
+ }
+ } else {
+ // Next month
+ if (month == 11) {
+ year++;
+ month = 0;
+ } else {
+ month++;
+ }
+ }
+ calendar.set(Calendar.MONTH, month);
+ calendar.set(Calendar.YEAR, year);
+ }
+
+ return calendar.get(Calendar.WEEK_OF_YEAR);
+ }
+
+ void setDetailedView(String detailedView) {
+ mDetailedView = detailedView;
+ }
+
+ void setSelectedTime(Time time) {
+ // Save the selected time so that we can restore it later when we switch views.
+ mSavedTime.set(time);
+
+ mViewCalendar.set(time);
+ mViewCalendar.monthDay = 1;
+ long millis = mViewCalendar.normalize(true /* ignore DST */);
+ mFirstJulianDay = Time.getJulianDay(millis, mViewCalendar.gmtoff);
+ mViewCalendar.set(time);
+
+ mCursor = new DayOfMonthCursor(time.year, time.month, time.monthDay,
+ mCursor.getWeekStartDay());
+
+ mRedrawScreen = true;
+ invalidate();
+ }
+
+ public long getSelectedTimeInMillis() {
+ Time time = mTempTime;
+ time.set(mViewCalendar);
+
+ time.month += mCursor.getSelectedMonthOffset();
+ time.monthDay = mCursor.getSelectedDayOfMonth();
+
+ // Restore the saved hour:minute:second offset from when we entered
+ // this view.
+ time.second = mSavedTime.second;
+ time.minute = mSavedTime.minute;
+ time.hour = mSavedTime.hour;
+ return time.normalize(true);
+ }
+
+ Time getTime() {
+ return mViewCalendar;
+ }
+
+ public int getSelectionMode() {
+ return mSelectionMode;
+ }
+
+ public void setSelectionMode(int selectionMode) {
+ mSelectionMode = selectionMode;
+ }
+
+ private void drawingCalc(int width, int height) {
+ mCellHeight = (height - (6 * WEEK_GAP)) / 6;
+ mEventGeometry.setHourHeight((mCellHeight - 25.0f * HOUR_GAP) / 24.0f);
+ mCellWidth = (width - (6 * MONTH_DAY_GAP)) / 7;
+ mBorder = (width - 6 * (mCellWidth + MONTH_DAY_GAP) - mCellWidth) / 2;
+
+ if (mShowToast) {
+ mPopup.dismiss();
+ mPopup.setWidth(width - 20);
+ mPopup.setHeight(POPUP_HEIGHT);
+ }
+
+ if (((mBitmap == null)
+ || mBitmap.isRecycled()
+ || (mBitmap.getHeight() != height)
+ || (mBitmap.getWidth() != width))
+ && (width > 0) && (height > 0)) {
+ if (mBitmap != null) {
+ mBitmap.recycle();
+ }
+ mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ mCanvas = new Canvas(mBitmap);
+ }
+
+ mBitmapRect.top = 0;
+ mBitmapRect.bottom = height;
+ mBitmapRect.left = 0;
+ mBitmapRect.right = width;
+ }
+
+ private void updateEventDetails(int date) {
+ if (!mShowToast) {
+ return;
+ }
+
+ getHandler().removeCallbacks(mDismissPopup);
+ ArrayList<Event> events = mEvents;
+ int numEvents = events.size();
+ if (numEvents == 0) {
+ mPopup.dismiss();
+ return;
+ }
+
+ int eventIndex = 0;
+ for (int i = 0; i < numEvents; i++) {
+ Event event = events.get(i);
+
+ if (event.startDay > date || event.endDay < date) {
+ continue;
+ }
+
+ // If we have all the event that we can display, then just count
+ // the extra ones.
+ if (eventIndex >= 4) {
+ eventIndex += 1;
+ continue;
+ }
+
+ int flags;
+ boolean showEndTime = false;
+ if (event.allDay) {
+ int numDays = event.endDay - event.startDay;
+ if (numDays == 0) {
+ flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL;
+ } else {
+ showEndTime = true;
+ flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_ABBREV_ALL;
+ }
+ } else {
+ flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
+ if (DateFormat.is24HourFormat(mContext)) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ }
+ }
+
+ String timeRange;
+ if (showEndTime) {
+ timeRange = DateUtils.formatDateRange(mParentActivity,
+ event.startMillis, event.endMillis, flags);
+ } else {
+ timeRange = DateUtils.formatDateRange(mParentActivity,
+ event.startMillis, event.startMillis, flags);
+ }
+
+ TextView timeView = null;
+ TextView titleView = null;
+ switch (eventIndex) {
+ case 0:
+ timeView = (TextView) mPopupView.findViewById(R.id.time0);
+ titleView = (TextView) mPopupView.findViewById(R.id.event_title0);
+ break;
+ case 1:
+ timeView = (TextView) mPopupView.findViewById(R.id.time1);
+ titleView = (TextView) mPopupView.findViewById(R.id.event_title1);
+ break;
+ case 2:
+ timeView = (TextView) mPopupView.findViewById(R.id.time2);
+ titleView = (TextView) mPopupView.findViewById(R.id.event_title2);
+ break;
+ case 3:
+ timeView = (TextView) mPopupView.findViewById(R.id.time3);
+ titleView = (TextView) mPopupView.findViewById(R.id.event_title3);
+ break;
+ }
+
+ timeView.setText(timeRange);
+ titleView.setText(event.title);
+ eventIndex += 1;
+ }
+ if (eventIndex == 0) {
+ // We didn't find any events for this day
+ mPopup.dismiss();
+ return;
+ }
+
+ // Hide the items that have no event information
+ View view;
+ switch (eventIndex) {
+ case 1:
+ view = mPopupView.findViewById(R.id.item_layout1);
+ view.setVisibility(View.GONE);
+ view = mPopupView.findViewById(R.id.item_layout2);
+ view.setVisibility(View.GONE);
+ view = mPopupView.findViewById(R.id.item_layout3);
+ view.setVisibility(View.GONE);
+ view = mPopupView.findViewById(R.id.plus_more);
+ view.setVisibility(View.GONE);
+ break;
+ case 2:
+ view = mPopupView.findViewById(R.id.item_layout1);
+ view.setVisibility(View.VISIBLE);
+ view = mPopupView.findViewById(R.id.item_layout2);
+ view.setVisibility(View.GONE);
+ view = mPopupView.findViewById(R.id.item_layout3);
+ view.setVisibility(View.GONE);
+ view = mPopupView.findViewById(R.id.plus_more);
+ view.setVisibility(View.GONE);
+ break;
+ case 3:
+ view = mPopupView.findViewById(R.id.item_layout1);
+ view.setVisibility(View.VISIBLE);
+ view = mPopupView.findViewById(R.id.item_layout2);
+ view.setVisibility(View.VISIBLE);
+ view = mPopupView.findViewById(R.id.item_layout3);
+ view.setVisibility(View.GONE);
+ view = mPopupView.findViewById(R.id.plus_more);
+ view.setVisibility(View.GONE);
+ break;
+ case 4:
+ view = mPopupView.findViewById(R.id.item_layout1);
+ view.setVisibility(View.VISIBLE);
+ view = mPopupView.findViewById(R.id.item_layout2);
+ view.setVisibility(View.VISIBLE);
+ view = mPopupView.findViewById(R.id.item_layout3);
+ view.setVisibility(View.VISIBLE);
+ view = mPopupView.findViewById(R.id.plus_more);
+ view.setVisibility(View.GONE);
+ break;
+ default:
+ view = mPopupView.findViewById(R.id.item_layout1);
+ view.setVisibility(View.VISIBLE);
+ view = mPopupView.findViewById(R.id.item_layout2);
+ view.setVisibility(View.VISIBLE);
+ view = mPopupView.findViewById(R.id.item_layout3);
+ view.setVisibility(View.VISIBLE);
+ TextView tv = (TextView) mPopupView.findViewById(R.id.plus_more);
+ tv.setVisibility(View.VISIBLE);
+ String format = mResources.getString(R.string.plus_N_more);
+ String plusMore = String.format(format, eventIndex - 4);
+ tv.setText(plusMore);
+ break;
+ }
+
+ if (eventIndex > 5) {
+ eventIndex = 5;
+ }
+ int popupHeight = 20 * eventIndex + 15;
+ mPopup.setHeight(popupHeight);
+
+ if (mPreviousPopupHeight != popupHeight) {
+ mPreviousPopupHeight = popupHeight;
+ mPopup.dismiss();
+ }
+ mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, 0, 0);
+ postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ long duration = event.getEventTime() - event.getDownTime();
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (mSelectionMode == SELECTION_HIDDEN) {
+ // Don't do anything unless the selection is visible.
+ break;
+ }
+
+ if (mSelectionMode == SELECTION_PRESSED) {
+ // This was the first press when there was nothing selected.
+ // Change the selection from the "pressed" state to the
+ // the "selected" state. We treat short-press and
+ // long-press the same here because nothing was selected.
+ mSelectionMode = SELECTION_SELECTED;
+ mRedrawScreen = true;
+ invalidate();
+ break;
+ }
+
+ // Check the duration to determine if this was a short press
+ if (duration < ViewConfiguration.getLongPressTimeout()) {
+ long millis = getSelectedTimeInMillis();
+ Utils.startActivity(getContext(), mDetailedView, millis);
+ mParentActivity.finish();
+ } else {
+ mSelectionMode = SELECTION_LONGPRESS;
+ mRedrawScreen = true;
+ invalidate();
+ performLongClick();
+ }
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (mSelectionMode == SELECTION_HIDDEN) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
+ || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP
+ || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
+ // Display the selection box but don't move or select it
+ // on this key press.
+ mSelectionMode = SELECTION_SELECTED;
+ mRedrawScreen = true;
+ invalidate();
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ // Display the selection box but don't select it
+ // on this key press.
+ mSelectionMode = SELECTION_PRESSED;
+ mRedrawScreen = true;
+ invalidate();
+ return true;
+ }
+ }
+
+ mSelectionMode = SELECTION_SELECTED;
+ boolean redraw = false;
+ Time other = null;
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ long millis = getSelectedTimeInMillis();
+ Utils.startActivity(getContext(), mDetailedView, millis);
+ mParentActivity.finish();
+ return true;
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (mCursor.up()) {
+ other = mOtherViewCalendar;
+ other.set(mViewCalendar);
+ other.month -= 1;
+ other.monthDay = mCursor.getSelectedDayOfMonth();
+
+ // restore the calendar cursor for the animation
+ mCursor.down();
+ }
+ redraw = true;
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (mCursor.down()) {
+ other = mOtherViewCalendar;
+ other.set(mViewCalendar);
+ other.month += 1;
+ other.monthDay = mCursor.getSelectedDayOfMonth();
+
+ // restore the calendar cursor for the animation
+ mCursor.up();
+ }
+ redraw = true;
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (mCursor.left()) {
+ other = mOtherViewCalendar;
+ other.set(mViewCalendar);
+ other.month -= 1;
+ other.monthDay = mCursor.getSelectedDayOfMonth();
+
+ // restore the calendar cursor for the animation
+ mCursor.right();
+ }
+ redraw = true;
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (mCursor.right()) {
+ other = mOtherViewCalendar;
+ other.set(mViewCalendar);
+ other.month += 1;
+ other.monthDay = mCursor.getSelectedDayOfMonth();
+
+ // restore the calendar cursor for the animation
+ mCursor.left();
+ }
+ redraw = true;
+ break;
+ }
+
+ if (other != null) {
+ other.normalize(true /* ignore DST */);
+ mNavigator.goTo(other);
+ } else if (redraw) {
+ mRedrawScreen = true;
+ invalidate();
+ }
+
+ return redraw;
+ }
+
+ class DismissPopup implements Runnable {
+ public void run() {
+ mPopup.dismiss();
+ }
+ }
+
+ // This is called when the activity is paused so that the popup can
+ // be dismissed.
+ void dismissPopup() {
+ if (!mShowToast) {
+ return;
+ }
+
+ // Protect against null-pointer exceptions
+ if (mPopup != null) {
+ mPopup.dismiss();
+ }
+
+ Handler handler = getHandler();
+ if (handler != null) {
+ handler.removeCallbacks(mDismissPopup);
+ }
+ }
+}
diff --git a/src/com/android/calendar/Navigator.java b/src/com/android/calendar/Navigator.java
new file mode 100644
index 00000000..e05a6033
--- /dev/null
+++ b/src/com/android/calendar/Navigator.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2008 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.text.format.Time;
+
+public interface Navigator {
+ /**
+ * Returns the time in millis of the selected event in this view.
+ * @return the selected time in UTC milliseconds.
+ */
+ long getSelectedTime();
+
+ /**
+ * Changes the view to include the given time.
+ * @param time the desired time to view.
+ */
+ void goTo(Time time);
+
+ /**
+ * Changes the view to include today's date.
+ */
+ void goToToday();
+
+ /**
+ * This is called when the user wants to create a new event and returns
+ * true if the new event should default to an all-day event.
+ * @return true if the new event should be an all-day event.
+ */
+ boolean getAllDay();
+}
diff --git a/src/com/android/calendar/SelectCalendarsActivity.java b/src/com/android/calendar/SelectCalendarsActivity.java
new file mode 100644
index 00000000..44943eb8
--- /dev/null
+++ b/src/com/android/calendar/SelectCalendarsActivity.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2007 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.app.Activity;
+import android.app.AlertDialog;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Calendar.Calendars;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.widget.AdapterView;
+import android.widget.CheckBox;
+import android.widget.ListView;
+
+
+public class SelectCalendarsActivity extends Activity implements ListView.OnItemClickListener {
+
+ private static final String TAG = "Calendar";
+ private View mView = null;
+ private Cursor mCursor = null;
+ private QueryHandler mQueryHandler;
+ private SelectCalendarsAdapter mAdapter;
+ private static final String[] PROJECTION = new String[] {
+ Calendars._ID,
+ Calendars.DISPLAY_NAME,
+ Calendars.COLOR,
+ Calendars.SELECTED,
+ Calendars.SYNC_EVENTS
+ };
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ setContentView(R.layout.calendars_activity);
+ getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
+ Window.PROGRESS_INDETERMINATE_ON);
+ mQueryHandler = new QueryHandler(getContentResolver());
+ mView = findViewById(R.id.calendars);
+ ListView items = (ListView) mView.findViewById(R.id.items);
+ Context context = mView.getContext();
+ mCursor = managedQuery(Calendars.CONTENT_URI, PROJECTION,
+ Calendars.SYNC_EVENTS + "=1",
+ null /* selectionArgs */,
+ Calendars.DEFAULT_SORT_ORDER);
+
+ mAdapter = new SelectCalendarsAdapter(context, mCursor);
+ items.setAdapter(mAdapter);
+ items.setOnItemClickListener(this);
+
+ // Start a background sync to get the list of calendars from the server.
+ startCalendarSync();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ }
+
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ CheckBox box = (CheckBox) view.findViewById(R.id.checkbox);
+ box.toggle();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ MenuItem item;
+ item = menu.add(0, 0, 0, R.string.add_calendars)
+ .setOnMenuItemClickListener(new ChangeCalendarAction(false /* not remove */));
+ item.setIcon(android.R.drawable.ic_menu_add);
+
+ item = menu.add(0, 0, 0, R.string.remove_calendars)
+ .setOnMenuItemClickListener(new ChangeCalendarAction(true /* remove */));
+ item.setIcon(android.R.drawable.ic_menu_delete);
+ return true;
+ }
+
+ /**
+ * ChangeCalendarAction is used both for adding and removing calendars.
+ * The constructor takes a boolean argument that is false if adding
+ * calendars and true if removing calendars. The user selects calendars
+ * to be added or removed from a pop-up list.
+ */
+ public class ChangeCalendarAction implements OnMenuItemClickListener,
+ DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener {
+
+ int mNumItems;
+ long[] mCalendarIds;
+ boolean[] mIsChecked;
+ ContentResolver mContentResolver;
+ boolean mRemove;
+
+ public ChangeCalendarAction(boolean remove) {
+ mContentResolver = SelectCalendarsActivity.this.getContentResolver();
+ mRemove = remove;
+ }
+
+ /*
+ * This is called when the user selects a calendar from either the
+ * "Add calendars" or "Remove calendars" popup dialog.
+ */
+ public void onClick(DialogInterface dialog, int position, boolean isChecked) {
+ mIsChecked[position] = isChecked;
+ }
+
+ /*
+ * This is called when the user presses the OK or Cancel button on the
+ * "Add calendars" or "Remove calendars" popup dialog.
+ */
+ public void onClick(DialogInterface dialog, int which) {
+ // If the user cancelled the dialog, then do nothing.
+ if (which == DialogInterface.BUTTON2) {
+ return;
+ }
+
+ boolean changesFound = false;
+ for (int position = 0; position < mNumItems; position++) {
+ // If this calendar wasn't selected, then skip it.
+ if (!mIsChecked[position]) {
+ continue;
+ }
+ changesFound = true;
+
+ long id = mCalendarIds[position];
+ Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
+ ContentValues values = new ContentValues();
+ int selected = 1;
+ if (mRemove) {
+ selected = 0;
+ }
+ values.put(Calendars.SELECTED, selected);
+ values.put(Calendars.SYNC_EVENTS, selected);
+ mContentResolver.update(uri, values, null, null);
+ }
+
+ // If there were any changes, then update the list of calendars
+ // that are synced.
+ if (changesFound) {
+ mCursor.requery();
+ }
+ }
+
+ public boolean onMenuItemClick(MenuItem item) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(SelectCalendarsActivity.this);
+ String selection;
+ if (mRemove) {
+ builder.setTitle(R.string.remove_calendars)
+ .setIcon(android.R.drawable.ic_dialog_alert);
+ selection = Calendars.SYNC_EVENTS + "=1";
+ } else {
+ builder.setTitle(R.string.add_calendars);
+ selection = Calendars.SYNC_EVENTS + "=0";
+ }
+ ContentResolver cr = getContentResolver();
+ Cursor cursor = cr.query(Calendars.CONTENT_URI, PROJECTION,
+ selection, null /* selectionArgs */,
+ Calendars.DEFAULT_SORT_ORDER);
+ if (cursor == null) {
+ Log.w(TAG, "Cannot get cursor for calendars");
+ return true;
+ }
+
+ int count = cursor.getCount();
+ mNumItems = count;
+ CharSequence[] calendarNames = new CharSequence[count];
+ mCalendarIds = new long[count];
+ mIsChecked = new boolean[count];
+ try {
+ int pos = 0;
+ while (cursor.moveToNext()) {
+ mCalendarIds[pos] = cursor.getLong(0);
+ calendarNames[pos] = cursor.getString(1);
+ pos += 1;
+ }
+ } finally {
+ cursor.close();
+ }
+
+ builder.setMultiChoiceItems(calendarNames, null, this)
+ .setPositiveButton(android.R.string.ok, this)
+ .setNegativeButton(android.R.string.cancel, this)
+ .show();
+ return true;
+ }
+ }
+
+ private class QueryHandler extends AsyncQueryHandler {
+ public QueryHandler(ContentResolver cr) {
+ super(cr);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
+ Window.PROGRESS_VISIBILITY_OFF);
+
+ // If the Activity is finishing, then close the cursor.
+ // Otherwise, use the new cursor in the adapter.
+ if (isFinishing()) {
+ stopManagingCursor(cursor);
+ cursor.close();
+ } else {
+ if (cursor.getCount() == 0) {
+ // There are no calendars. This might happen if we lost
+ // the wireless connection (in airplane mode, for example).
+ // Leave the current list of calendars alone and pop up
+ // a dialog explaining that the connection is down.
+ // But allow the user to add and remove calendars.
+ return;
+ }
+ if (mCursor != null) {
+ stopManagingCursor(mCursor);
+ }
+ mCursor = cursor;
+ startManagingCursor(cursor);
+ mAdapter.changeCursor(cursor);
+ }
+ }
+ }
+
+ // This class implements the menu option "Refresh list from server".
+ // (No longer used.)
+ public class RefreshAction implements Runnable {
+ public void run() {
+ startCalendarSync();
+ }
+ }
+
+ // startCalendarSync() checks the server for an updated list of Calendars
+ // (in the background) using an AsyncQueryHandler.
+ //
+ // Calendars are never removed from the phone due to a server sync.
+ // But if a Calendar is added on the web (and it is selected and not
+ // hidden) then it will be added to the list of calendars on the phone
+ // (when this asynchronous query finishes). When a new calendar from the
+ // web is added to the phone, then the events for that calendar are also
+ // downloaded from the web.
+ //
+ // This sync is done automatically in the background when the
+ // SelectCalendars activity is started.
+ private void startCalendarSync() {
+ getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
+ Window.PROGRESS_VISIBILITY_ON);
+
+ // TODO: make sure the user has login info.
+
+ Uri uri = Calendars.LIVE_CONTENT_URI;
+ mQueryHandler.startQuery(0, null, uri, PROJECTION,
+ Calendars.SYNC_EVENTS + "=1",
+ null, Calendars.DEFAULT_SORT_ORDER);
+ }
+}
diff --git a/src/com/android/calendar/SelectCalendarsAdapter.java b/src/com/android/calendar/SelectCalendarsAdapter.java
new file mode 100644
index 00000000..fb50662a
--- /dev/null
+++ b/src/com/android/calendar/SelectCalendarsAdapter.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2007 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.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.net.Uri;
+import android.provider.Calendar.Calendars;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CursorAdapter;
+import android.widget.TextView;
+
+public class SelectCalendarsAdapter extends CursorAdapter {
+
+ private static final int CLEAR_ALPHA_MASK = 0x00FFFFFF;
+ private static final int HIGH_ALPHA = 255 << 24;
+ private static final int MED_ALPHA = 180 << 24;
+ private static final int LOW_ALPHA = 150 << 24;
+
+ /* The corner should be rounded on the top right and bottom right */
+ private static final float[] CORNERS = new float[] {0, 0, 5, 5, 5, 5, 0, 0};
+
+ private static final String TAG = "Calendar";
+
+ private final LayoutInflater mInflater;
+ private final ContentResolver mResolver;
+ private final ContentValues mValues = new ContentValues();
+
+ private class CheckBoxListener implements CheckBox.OnCheckedChangeListener {
+ private final long mCalendarId;
+
+ private CheckBoxListener(long calendarId) {
+ mCalendarId = calendarId;
+ }
+
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, mCalendarId);
+ mValues.clear();
+ int checked = isChecked ? 1 : 0;
+ mValues.put(Calendars.SELECTED, checked);
+ mResolver.update(uri, mValues, null, null);
+ }
+ }
+
+ public SelectCalendarsAdapter(Context context, Cursor cursor) {
+ super(context, cursor);
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mResolver = context.getContentResolver();
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return mInflater.inflate(R.layout.calendar_item, parent, false);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ int idColumn = cursor.getColumnIndexOrThrow(Calendars._ID);
+ int nameColumn = cursor.getColumnIndexOrThrow(Calendars.DISPLAY_NAME);
+ int selectedColumn = cursor.getColumnIndexOrThrow(Calendars.SELECTED);
+ int colorColumn = cursor.getColumnIndexOrThrow(Calendars.COLOR);
+ view.findViewById(R.id.color).setBackgroundDrawable(getColorChip(cursor.getInt(colorColumn)));
+ setText(view, R.id.calendar, cursor.getString(nameColumn));
+ CheckBox box = (CheckBox) view.findViewById(R.id.checkbox);
+ long id = cursor.getLong(idColumn);
+ boolean checked = cursor.getInt(selectedColumn) != 0;
+ box.setOnCheckedChangeListener(null);
+ box.setChecked(checked);
+ box.setOnCheckedChangeListener(new CheckBoxListener(id));
+ }
+
+ private static void setText(View view, int id, String text) {
+ if (TextUtils.isEmpty(text)) {
+ return;
+ }
+ TextView textView = (TextView) view.findViewById(id);
+ textView.setText(text);
+ }
+
+ private Drawable getColorChip(int color) {
+
+ /*
+ * We want the color chip to have a nice gradient using
+ * the color of the calendar. To do this we use a GradientDrawable.
+ * The color supplied has an alpha of FF so we first do:
+ * color & 0x00FFFFFF
+ * to clear the alpha. Then we add our alpha to it.
+ * We use 3 colors to get a step effect where it starts off very
+ * light and quickly becomes dark and then a slow transition to
+ * be even darker.
+ */
+ color &= CLEAR_ALPHA_MASK;
+ int startColor = color | HIGH_ALPHA;
+ int middleColor = color | MED_ALPHA;
+ int endColor = color | LOW_ALPHA;
+ int[] colors = new int[] {startColor, middleColor, endColor};
+ GradientDrawable d = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, colors);
+ d.setCornerRadii(CORNERS);
+ return d;
+ }
+}
diff --git a/src/com/android/calendar/Utils.java b/src/com/android/calendar/Utils.java
new file mode 100644
index 00000000..53c584cb
--- /dev/null
+++ b/src/com/android/calendar/Utils.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2006 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 static android.provider.Calendar.EVENT_BEGIN_TIME;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.text.format.Time;
+import android.view.animation.AlphaAnimation;
+import android.widget.ViewFlipper;
+
+public class Utils {
+ public static void startActivity(Context context, String className, long time) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+
+ intent.setClassName(context, className);
+ intent.putExtra(EVENT_BEGIN_TIME, time);
+
+ context.startActivity(intent);
+ }
+
+ public static final Time timeFromIntent(Intent intent) {
+ Time time = new Time();
+ time.set(timeFromIntentInMillis(intent));
+ return time;
+ }
+
+ /**
+ * If the given intent specifies a time (in milliseconds since the epoch),
+ * then that time is returned. Otherwise, the current time is returned.
+ */
+ public static final long timeFromIntentInMillis(Intent intent) {
+ // If the time was specified, then use that. Otherwise, use the current time.
+ long millis = intent.getLongExtra(EVENT_BEGIN_TIME, -1);
+ if (millis == -1) {
+ millis = System.currentTimeMillis();
+ }
+ return millis;
+ }
+
+ public static final void applyAlphaAnimation(ViewFlipper v) {
+ AlphaAnimation in = new AlphaAnimation(0.0f, 1.0f);
+
+ in.setStartOffset(0);
+ in.setDuration(500);
+
+ AlphaAnimation out = new AlphaAnimation(1.0f, 0.0f);
+
+ out.setStartOffset(0);
+ out.setDuration(500);
+
+ v.setInAnimation(in);
+ v.setOutAnimation(out);
+ }
+
+ /**
+ * Formats the given Time object so that it gives the month and year
+ * (for example, "September 2007").
+ *
+ * @param time the time to format
+ * @return the string containing the weekday and the date
+ */
+ public static String formatMonthYear(Time time) {
+ Resources res = Resources.getSystem();
+ return time.format(res.getString(com.android.internal.R.string.month_year));
+ }
+
+ // TODO: replace this with the correct i18n way to do this
+ public static final String englishNthDay[] = {
+ "", "1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th",
+ "10th", "11th", "12th", "13th", "14th", "15th", "16th", "17th", "18th", "19th",
+ "20th", "21st", "22nd", "23rd", "24th", "25th", "26th", "27th", "28th", "29th",
+ "30th", "31st"
+ };
+
+ public static String formatNth(int nth) {
+ return "the " + englishNthDay[nth];
+ }
+
+ /**
+ * Sets the time to the beginning of the day (midnight) by clearing the
+ * hour, minute, and second fields.
+ */
+ static void setTimeToStartOfDay(Time time) {
+ time.second = 0;
+ time.minute = 0;
+ time.hour = 0;
+ }
+}
diff --git a/src/com/android/calendar/WeekActivity.java b/src/com/android/calendar/WeekActivity.java
new file mode 100644
index 00000000..0e06b518
--- /dev/null
+++ b/src/com/android/calendar/WeekActivity.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2006 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.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.ProgressBar;
+import android.widget.ViewSwitcher;
+
+public class WeekActivity extends CalendarActivity implements ViewSwitcher.ViewFactory {
+ /**
+ * The view id used for all the views we create. It's OK to have all child
+ * views have the same ID. This ID is used to pick which view receives
+ * focus when a view hierarchy is saved / restore
+ */
+ private static final int VIEW_ID = 1;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.week_activity);
+
+ mSelectedDay = Utils.timeFromIntent(getIntent());
+ mViewSwitcher = (ViewSwitcher) findViewById(R.id.switcher);
+ mViewSwitcher.setFactory(this);
+ mViewSwitcher.getCurrentView().requestFocus();
+ mProgressBar = (ProgressBar) findViewById(R.id.progress_circular);
+ }
+
+ public View makeView() {
+ WeekView wv = new WeekView(this);
+ wv.setId(VIEW_ID);
+ wv.setLayoutParams(new ViewSwitcher.LayoutParams(
+ LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
+ wv.setSelectedDay(mSelectedDay);
+ return wv;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ CalendarView view1 = (CalendarView) mViewSwitcher.getCurrentView();
+ CalendarView view2 = (CalendarView) mViewSwitcher.getNextView();
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ String str = prefs.getString(CalendarPreferenceActivity.KEY_DETAILED_VIEW,
+ CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW);
+ view1.setDetailedView(str);
+ view2.setDetailedView(str);
+
+ // Record Week View as the (new) start view
+ String activityString = CalendarApplication.ACTIVITY_NAMES[CalendarApplication.WEEK_VIEW_ID];
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(CalendarPreferenceActivity.KEY_START_VIEW, activityString);
+ editor.commit();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ CalendarView view = (CalendarView) mViewSwitcher.getCurrentView();
+ mSelectedDay = view.getSelectedDay();
+ }
+}
diff --git a/src/com/android/calendar/WeekView.java b/src/com/android/calendar/WeekView.java
new file mode 100644
index 00000000..b0b32445
--- /dev/null
+++ b/src/com/android/calendar/WeekView.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2006 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;
+
+
+public class WeekView extends CalendarView {
+ private static final int CELL_MARGIN = 0;
+
+ public WeekView(CalendarActivity activity) {
+ super(activity);
+ init();
+ }
+
+ private void init() {
+ mDrawTextInEventRect = false;
+ mNumDays = 7;
+ mEventGeometry.setCellMargin(CELL_MARGIN);
+ }
+}