Initial implementation of guest invitation and status
Added a number of TODO's which will be taken care of later. Performance
needs to be improved. Also changed some of the content provider calls
to the batch API.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 4fcd945..8805d4c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -24,6 +24,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE"/>
+ <uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
@@ -58,7 +59,7 @@
android:exported="true" />
<activity android:name="EditEvent" android:label="@string/event_edit_title"
- android:theme="@android:style/Theme.Light"
+ android:theme="@android:style/Theme"
android:configChanges="orientation|keyboardHidden">
<intent-filter>
diff --git a/res/drawable/im_avatar_picture_border_normal.9.png b/res/drawable/im_avatar_picture_border_normal.9.png
new file mode 100644
index 0000000..01cc9dc
--- /dev/null
+++ b/res/drawable/im_avatar_picture_border_normal.9.png
Binary files differ
diff --git a/res/layout/contact_item.xml b/res/layout/contact_item.xml
new file mode 100644
index 0000000..1d37f52
--- /dev/null
+++ b/res/layout/contact_item.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:paddingLeft="2dip"
+ android:paddingRight="10dip"
+ android:minHeight="46dip">
+
+ <ImageView
+ android:id="@+id/avatar"
+ android:scaleType="centerCrop"
+ android:paddingLeft="4dip"
+ android:layout_width="46dip"
+ android:layout_height="46dip"
+ android:layout_marginLeft="10dip"
+ android:layout_marginTop="10dip"
+ android:layout_marginBottom="10dip"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:background="@drawable/im_avatar_picture_border_normal" />
+
+ <TextView
+ android:id="@+id/name"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:inputType="none"
+ android:paddingLeft="5dip"
+ android:layout_centerVertical="true"
+ android:layout_toRightOf="@id/avatar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" />
+
+ <ImageView
+ android:id="@+id/presence"
+ android:scaleType="fitXY"
+ android:visibility="gone"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <View
+ android:id="@+id/separator"
+ android:layout_width="fill_parent"
+ android:layout_height="1dip"
+ android:layout_alignParentBottom="true"
+ android:background="@android:drawable/divider_horizontal_bright" />
+
+</RelativeLayout>
diff --git a/res/layout/edit_event.xml b/res/layout/edit_event.xml
index 352a4ed..2dd4a4f 100644
--- a/res/layout/edit_event.xml
+++ b/res/layout/edit_event.xml
@@ -68,14 +68,14 @@
android:layout_height="wrap_content"
android:layout_weight="7"
android:gravity="left|center_vertical"
- style="?android:attr/textAppearanceMedium"/>
+ style="?android:attr/textAppearanceMediumInverse"/>
<Button android:id="@+id/start_time"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_weight="4"
android:gravity="left|center_vertical"
- style="?android:attr/textAppearanceMedium"/>
+ style="?android:attr/textAppearanceMediumInverse"/>
</LinearLayout>
@@ -95,14 +95,14 @@
android:layout_height="wrap_content"
android:layout_weight="7"
android:gravity="left|center_vertical"
- style="?android:attr/textAppearanceMedium"/>
+ style="?android:attr/textAppearanceMediumInverse"/>
<Button android:id="@+id/end_time"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_weight="4"
android:gravity="left|center_vertical"
- style="?android:attr/textAppearanceMedium"/>
+ style="?android:attr/textAppearanceMediumInverse"/>
</LinearLayout>
<LinearLayout
@@ -116,7 +116,7 @@
android:text="@string/edit_event_all_day_label"
android:paddingTop="1dip"
android:paddingRight="7dip"
- style="?android:attr/textAppearanceMedium"/>
+ style="?android:attr/textAppearanceMediumInverse"/>
<CheckBox android:id="@+id/is_all_day"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -195,6 +195,29 @@
android:layout_height="wrap_content" />
</LinearLayout>
+ <!-- GUESTS/ATTENDEES -->
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ style="@style/EditEvent_Layout">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/attendees_label"
+ style="@style/TextAppearance.EditEvent_Label"/>
+
+ <MultiAutoCompleteTextView android:id="@+id/attendees"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textEmailAddress|textMultiLine"
+ android:hint="@string/hint_attendees"
+ android:layout_marginTop="6dip"
+ android:layout_marginBottom="6dip"
+ android:imeOptions="actionNext"/>
+ </LinearLayout>
+
<!-- REPEATS -->
<LinearLayout
android:orientation="vertical"
@@ -294,7 +317,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="2dip"
- android:text="@string/add_new_reminder"/>
+ android:text="@string/add_new_reminder"
+ style="?android:attr/textAppearanceSmallInverse"/>
<ImageButton android:id="@+id/reminder_add"
style="@style/PlusButton"
diff --git a/res/layout/event_info_activity.xml b/res/layout/event_info_activity.xml
index 3992e80..a6b4d73 100644
--- a/res/layout/event_info_activity.xml
+++ b/res/layout/event_info_activity.xml
@@ -154,7 +154,15 @@
</LinearLayout>
</LinearLayout>
</LinearLayout>
-
+
+ <!-- GUEST LIST -->
+ <LinearLayout
+ android:id="@+id/attendee_list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="8dip"
+ android:orientation="vertical" />
+
<!-- RESPONSE -->
<LinearLayout android:id="@+id/response_container"
android:orientation="vertical"
@@ -169,7 +177,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/view_event_response_label"
- style="@style/TextAppearance.EditEvent_Label"/>
+ style="@style/TextAppearance.EventInfo_Label"/>
<Spinner android:id="@+id/response_value"
style="?android:attr/textAppearanceMedium"
@@ -193,7 +201,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reminders_label"
- style="@style/TextAppearance.EditEvent_Label"/>
+ style="@style/TextAppearance.EventInfo_Label"/>
<LinearLayout android:id="@+id/reminder_items_container"
style="?android:attr/textAppearanceMedium"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c463ef6..4286874 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -30,6 +30,11 @@
<string name="when_label">When</string>
<!-- This is the label for the location of an event -->
<string name="where_label">Where</string>
+ <!-- This is the label for the Guests/Attendees of an event -->
+ <string name="attendees_label">Guests</string>
+ <!-- This is the label for the Guest Responses and count of an event e.g. Yes (3) -->
+ <string name="response_label">"<xliff:g id="response_type">%s</xliff:g> (<xliff:g id="guest_count">%d</xliff:g>)"</string>
+
<!-- Some events repeat daily, weekly, monthly, or yearly. This is the label
for all the choices about how often an event repeats (including the choice
of not repeating). -->
@@ -151,6 +156,8 @@
<string name="hint_where">"Event location"</string>
<!-- Default value of Description field (as a hint to the user) -->
<string name="hint_description">"Event description"</string>
+ <!-- Default value of Attendees/Guests field (as a hint to the user) -->
+ <string name="hint_attendees">"Email addresses"</string>
<string name="creating_event">"Creating event\u2026"</string>
<string name="saving_event">"Saving event\u2026"</string>
<string name="loading_calendars_title">"Loading calendars"</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 4471ad2..0f6e89d 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -59,6 +59,13 @@
<style name="TextAppearance.EditEvent_Label">
<item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
+ <item name="android:textColor">?android:attr/textColorSecondaryInverse</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:paddingLeft">2dip</item>
+ </style>
+
+ <style name="TextAppearance.EventInfo_Label">
+ <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
<item name="android:textColor">?android:attr/textColorSecondary</item>
<item name="android:textStyle">bold</item>
<item name="android:paddingLeft">2dip</item>
diff --git a/src/com/android/calendar/EditEvent.java b/src/com/android/calendar/EditEvent.java
index a772bb7..1451218 100644
--- a/src/com/android/calendar/EditEvent.java
+++ b/src/com/android/calendar/EditEvent.java
@@ -18,6 +18,7 @@
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;
@@ -26,28 +27,40 @@
import android.app.DatePickerDialog.OnDateSetListener;
import android.app.TimePickerDialog.OnTimeSetListener;
import android.content.AsyncQueryHandler;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
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.OperationApplicationException;
import android.content.SharedPreferences;
+import android.content.ContentProviderOperation.Builder;
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.os.RemoteException;
import android.pim.EventRecurrence;
import android.preference.PreferenceManager;
+import android.provider.Calendar.Attendees;
import android.provider.Calendar.Calendars;
import android.provider.Calendar.Events;
import android.provider.Calendar.Reminders;
+import android.text.InputFilter;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.text.format.Time;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+import android.text.util.Rfc822Validator;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -62,6 +75,7 @@
import android.widget.DatePicker;
import android.widget.ImageButton;
import android.widget.LinearLayout;
+import android.widget.MultiAutoCompleteTextView;
import android.widget.ResourceCursorAdapter;
import android.widget.Spinner;
import android.widget.TextView;
@@ -75,6 +89,8 @@
public class EditEvent extends Activity implements View.OnClickListener,
DialogInterface.OnCancelListener, DialogInterface.OnClickListener {
+ private static final boolean DEBUG = false;
+
/**
* 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.
@@ -156,7 +172,7 @@
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
@@ -184,6 +200,9 @@
private LinearLayout mExtraOptions;
private ArrayList<Integer> mOriginalMinutes = new ArrayList<Integer>();
private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0);
+ MultiAutoCompleteTextView mAttendeesList;
+ private EmailAddressAdapter mAddressAdapter;
+ private Rfc822Validator mValidator;
private EventRecurrence mEventRecurrence = new EventRecurrence();
private String mRrule;
@@ -214,7 +233,7 @@
private DeleteEventHelper mDeleteEventHelper;
private QueryHandler mQueryHandler;
-
+
/* This class is used to update the time buttons. */
private class TimeListener implements OnTimeSetListener {
private View mView;
@@ -373,7 +392,7 @@
}
return;
}
-
+
if (v == mDeleteButton) {
long begin = mStartTime.toMillis(false /* use isDst */);
long end = mEndTime.toMillis(false /* use isDst */);
@@ -392,12 +411,12 @@
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();
@@ -426,7 +445,7 @@
finish();
}
}
-
+
private class QueryHandler extends AsyncQueryHandler {
public QueryHandler(ContentResolver cr) {
super(cr);
@@ -442,7 +461,7 @@
} else {
mCalendarsCursor = cursor;
startManagingCursor(cursor);
-
+
// Stop the spinner
getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
Window.PROGRESS_VISIBILITY_OFF);
@@ -454,7 +473,7 @@
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);
@@ -514,7 +533,7 @@
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);
@@ -527,7 +546,7 @@
// 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);
@@ -584,6 +603,10 @@
mRemindersContainer = (LinearLayout) findViewById(R.id.reminder_items_container);
mExtraOptions = (LinearLayout) findViewById(R.id.extra_options_container);
+ mAddressAdapter = new EmailAddressAdapter(this);
+ mValidator = new Rfc822Validator("google.com"); //TODO use user's domain
+ mAttendeesList = initMultiAutoCompleteTextView(R.id.attendees, R.string.hint_attendees);
+
mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
@@ -664,7 +687,7 @@
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()) {
@@ -684,7 +707,7 @@
public void onClick(View v) {
addReminder();
}
- };
+ };
ImageButton reminderRemoveButton = (ImageButton) findViewById(R.id.reminder_add);
reminderRemoveButton.setOnClickListener(addReminderOnClickListener);
@@ -696,33 +719,103 @@
initFromIntent(intent);
}
}
-
+
+ private Rfc822Token[] getAddressesFromList(MultiAutoCompleteTextView list) {
+ list.clearComposingText();
+ return Rfc822Tokenizer.tokenize(list.getText());
+ }
+
+ // From com.google.android.gm.ComposeActivity
+ private MultiAutoCompleteTextView initMultiAutoCompleteTextView(int res, int hintId) {
+ MultiAutoCompleteTextView list = (MultiAutoCompleteTextView) findViewById(res);
+ list.setAdapter(mAddressAdapter);
+ list.setTokenizer(new Rfc822Tokenizer());
+ list.setValidator(mValidator);
+
+ // NOTE: assumes no other filters are set
+ list.setFilters(sRecipientFilters);
+
+ return list;
+ }
+
+ /**
+ * From com.google.android.gm.ComposeActivity
+ * Implements special address cleanup rules:
+ * The first space key entry following an "@" symbol that is followed by any combination
+ * of letters and symbols, including one+ dots and zero commas, should insert an extra
+ * comma (followed by the space).
+ */
+ private static InputFilter[] sRecipientFilters = new InputFilter[] { new InputFilter() {
+
+ public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
+ int dstart, int dend) {
+
+ // quick check - did they enter a single space?
+ if (end-start != 1 || source.charAt(start) != ' ') {
+ return null;
+ }
+
+ // determine if the characters before the new space fit the pattern
+ // follow backwards and see if we find a comma, dot, or @
+ int scanBack = dstart;
+ boolean dotFound = false;
+ while (scanBack > 0) {
+ char c = dest.charAt(--scanBack);
+ switch (c) {
+ case '.':
+ dotFound = true; // one or more dots are req'd
+ break;
+ case ',':
+ return null;
+ case '@':
+ if (!dotFound) {
+ return null;
+ }
+ // we have found a comma-insert case. now just do it
+ // in the least expensive way we can.
+ if (source instanceof Spanned) {
+ SpannableStringBuilder sb = new SpannableStringBuilder(",");
+ sb.append(source);
+ return sb;
+ } else {
+ return ", ";
+ }
+ default:
+ // just keep going
+ }
+ }
+
+ // no termination cases were found, so don't edit the input
+ return null;
+ }
+ }};
+
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;
@@ -741,7 +834,7 @@
return;
}
}
-
+
if (mEventCursor != null) {
Cursor cursor = mEventCursor;
cursor.moveToFirst();
@@ -795,7 +888,7 @@
} 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) {
@@ -1069,7 +1162,7 @@
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));
@@ -1077,7 +1170,7 @@
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);
@@ -1088,17 +1181,17 @@
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++) {
@@ -1108,14 +1201,14 @@
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
@@ -1129,7 +1222,7 @@
}
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
@@ -1137,7 +1230,7 @@
static String constructReminderLabel(Context context, int minutes, boolean abbrev) {
Resources resources = context.getResources();
int value, resId;
-
+
if (minutes % 60 != 0) {
value = minutes;
if (abbrev) {
@@ -1185,7 +1278,7 @@
// Saves the event. Returns true if it is okay to exit this activity.
private boolean save() {
boolean forceSaveReminders = false;
-
+
// 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) {
@@ -1215,7 +1308,9 @@
Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show();
}
- ContentResolver cr = getContentResolver();
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+ int eventIdIndex = -1;
+
ContentValues values = getContentValuesFromUi();
Uri uri = mUri;
@@ -1224,21 +1319,23 @@
if (uri == null) {
// Create new event with new contents
addRecurrenceRule(values);
- uri = cr.insert(Events.CONTENT_URI, values);
+ eventIdIndex = ops.size();
+ Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
+ ops.add(b.build());
forceSaveReminders = true;
} else if (mRrule == null) {
// Modify contents of a non-repeating event
addRecurrenceRule(values);
checkTimeDependentFields(values);
- cr.update(uri, values, null, null);
-
+ ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
+
} 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);
+ ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
} else if (mModification == MODIFY_SELECTED) {
// Modify contents of the current instance of repeating event
@@ -1250,7 +1347,9 @@
boolean allDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
- uri = cr.insert(Events.CONTENT_URI, values);
+ eventIdIndex = ops.size();
+ Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
+ ops.add(b.build());
forceSaveReminders = true;
} else if (mModification == MODIFY_ALL_FOLLOWING) {
@@ -1263,56 +1362,127 @@
// then delete the whole series. Otherwise, update the series
// to end at the new start time.
if (isFirstEventInSeries()) {
- cr.delete(uri, null, null);
+ ops.add(ContentProviderOperation.newDelete(uri).build());
} else {
// Update the current repeating event to end at the new
// start time.
- updatePastEvents(cr, uri);
+ updatePastEvents(ops, uri);
}
- uri = cr.insert(Events.CONTENT_URI, values);
+ eventIdIndex = ops.size();
+ ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
+ .build());
} else {
if (isFirstEventInSeries()) {
checkTimeDependentFields(values);
values.remove(Events.DTEND);
- cr.update(uri, values, null, null);
+ Builder b = ContentProviderOperation.newUpdate(uri).withValues(values);
+ ops.add(b.build());
} else {
// Update the current repeating event to end at the new
// start time.
- updatePastEvents(cr, uri);
+ updatePastEvents(ops, uri);
// Create a new event with the user-modified fields
values.remove(Events.DTEND);
- uri = cr.insert(Events.CONTENT_URI, values);
+ eventIdIndex = ops.size();
+ ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(
+ values).build());
}
}
forceSaveReminders = true;
} 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);
+ ops.add(ContentProviderOperation.newDelete(uri).build());
+
+ eventIdIndex = ops.size();
+ ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
+ .build());
forceSaveReminders = true;
} else {
checkTimeDependentFields(values);
values.remove(Events.DTEND);
- cr.update(uri, values, null, null);
+ ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
}
}
- if (uri != null) {
+ ArrayList<Integer> reminderMinutes = reminderItemsToMinutes(mReminderItems,
+ mReminderValues);
+ if (eventIdIndex != -1) {
+ saveRemindersWithBackRef(ops, eventIdIndex, reminderMinutes, mOriginalMinutes,
+ forceSaveReminders);
+ } else if (uri != null) {
long eventId = ContentUris.parseId(uri);
- ArrayList<Integer> reminderMinutes = reminderItemsToMinutes(mReminderItems,
- mReminderValues);
- saveReminders(cr, eventId, reminderMinutes, mOriginalMinutes,
+ saveReminders(ops, eventId, reminderMinutes, mOriginalMinutes,
forceSaveReminders);
}
+
+ if (eventIdIndex != -1 || uri != null) {
+ // Delete all the existing attendees for this event
+ Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
+
+ long eventId = -1;
+ if (eventIdIndex == -1) {
+ eventId = ContentUris.parseId(uri);
+ String where = Attendees.EVENT_ID + "=?";
+ String[] args = new String[] {
+ Long.toString(eventId)
+ };
+ b.withSelection(where, args);
+ } else {
+ // Delete all the existing reminders for this event
+ b.withSelection(Attendees.EVENT_ID + "=?", new String[1]);
+ b.withSelectionBackReference(0, eventIdIndex);
+ }
+ ops.add(b.build());
+
+ if (mAttendeesList.getText().length() > 0) {
+ Rfc822Token[] attendees = getAddressesFromList(mAttendeesList);
+ // Insert the attendees
+ for (Rfc822Token attendee : attendees) {
+ values.clear();
+ values.put(Attendees.ATTENDEE_NAME, attendee.getName());
+ values.put(Attendees.ATTENDEE_EMAIL, attendee.getAddress());
+ values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
+ values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
+ values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_NONE);
+
+ if (eventIdIndex != -1) {
+ b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
+ .withValues(values);
+ b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
+ } else {
+ values.put(Attendees.EVENT_ID, eventId);
+ b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
+ .withValues(values);
+ }
+ ops.add(b.build());
+ }
+ }
+ }
+
+ try {
+ // TODO Move this to background thread
+ ContentProviderResult[] results =
+ getContentResolver().applyBatch(android.provider.Calendar.AUTHORITY, ops);
+ if (DEBUG) {
+ Log.v("=====", "results = " + Arrays.toString(results));
+ }
+ } catch (RemoteException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (OperationApplicationException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
return true;
}
@@ -1322,7 +1492,7 @@
return start == mStartTime.toMillis(true);
}
- private void updatePastEvents(ContentResolver cr, Uri uri) {
+ private void updatePastEvents(ArrayList<ContentProviderOperation> ops, 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;
@@ -1338,17 +1508,17 @@
// 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)
+ 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.
@@ -1364,7 +1534,8 @@
oldValues.put(Events.DTSTART, oldStartMillis);
oldValues.put(Events.DURATION, oldDuration);
oldValues.put(Events.RRULE, mEventRecurrence.toString());
- cr.update(uri, oldValues, null, null);
+ Builder b = ContentProviderOperation.newUpdate(uri).withValues(oldValues);
+ ops.add(b.build());
}
private void checkTimeDependentFields(ContentValues values) {
@@ -1373,13 +1544,13 @@
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)
@@ -1414,7 +1585,7 @@
values.put(Events.DTSTART, oldStartMillis);
}
}
-
+
static ArrayList<Integer> reminderItemsToMinutes(ArrayList<LinearLayout> reminderItems,
ArrayList<Integer> reminderValues) {
int len = reminderItems.size();
@@ -1431,8 +1602,8 @@
/**
* Saves the reminders, if they changed. Returns true if the database
* was updated.
- *
- * @param cr the ContentResolver
+ *
+ * @param ops the array of ContentProviderOperations
* @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
@@ -1440,7 +1611,7 @@
* change
* @return true if the database was updated
*/
- static boolean saveReminders(ContentResolver cr, long eventId,
+ static boolean saveReminders(ArrayList<ContentProviderOperation> ops, long eventId,
ArrayList<Integer> reminderMinutes, ArrayList<Integer> originalMinutes,
boolean forceSave) {
// If the reminders have not changed, then don't update the database
@@ -1448,28 +1619,70 @@
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);
+ // TODO re-enable this
+// // Delete all the existing reminders for this event
+// String where = Reminders.EVENT_ID + "=?";
+// String[] args = new String[] { Long.toString(eventId) };
+// Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
+// b.withSelection(where, args);
+// ops.add(b.build());
+//
+// // 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);
+// ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
+//
+// // 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);
+// b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
+// ops.add(b.build());
+// }
+ return true;
+ }
- // 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);
+ static boolean saveRemindersWithBackRef(ArrayList<ContentProviderOperation> ops,
+ int eventIdIndex, ArrayList<Integer> reminderMinutes,
+ ArrayList<Integer> originalMinutes, boolean forceSave) {
+ // If the reminders have not changed, then don't update the database
+ if (reminderMinutes.equals(originalMinutes) && !forceSave) {
+ return false;
}
+
+ // TODO re-enable this
+// // Delete all the existing reminders for this event
+// Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
+// b.withSelection(Reminders.EVENT_ID + "=?", new String[1]);
+// b.withSelectionBackReference(0, eventIdIndex);
+// ops.add(b.build());
+//
+// // Update the "hasAlarm" field for the event
+// ContentValues values = new ContentValues();
+// int len = reminderMinutes.size();
+// values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0);
+// b = ContentProviderOperation.newUpdate(Events.CONTENT_URI).withValues(values);
+// b.withSelection(Events._ID + "=?", new String[1]);
+// b.withSelectionBackReference(0, eventIdIndex);
+// ops.add(b.build());
+//
+// // 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);
+// b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
+// b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
+// ops.add(b.build());
+// }
return true;
}
@@ -1479,7 +1692,7 @@
if (mRrule == null) {
return;
}
-
+
values.put(Events.RRULE, mRrule);
long end = mEndTime.toMillis(true /* ignore dst */);
long start = mStartTime.toMillis(true /* ignore dst */);
@@ -1599,7 +1812,7 @@
mEndTime.monthDay++;
mEndTime.timezone = timezone;
endMillis = mEndTime.normalize(true);
-
+
if (mEventCursor == null) {
// This is a new event
calendarId = mCalendarsSpinner.getSelectedItemId();
@@ -1612,7 +1825,7 @@
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.
@@ -1623,7 +1836,7 @@
} 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();
diff --git a/src/com/android/calendar/EmailAddressAdapter.java b/src/com/android/calendar/EmailAddressAdapter.java
new file mode 100644
index 0000000..b0b58ba
--- /dev/null
+++ b/src/com/android/calendar/EmailAddressAdapter.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2007 Google Inc.
+ *
+ * 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.Contacts.ContactMethods.CONTENT_EMAIL_URI;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.provider.Contacts.ContactMethods;
+import android.provider.Contacts.People;
+import android.text.TextUtils;
+import android.text.util.Rfc822Token;
+import android.view.View;
+import android.widget.ResourceCursorAdapter;
+import android.widget.TextView;
+
+public class EmailAddressAdapter extends ResourceCursorAdapter {
+ public static final int NAME_INDEX = 1;
+ public static final int DATA_INDEX = 2;
+
+ private static final String SORT_ORDER = People.TIMES_CONTACTED + " DESC, " + People.NAME;
+ private ContentResolver mContentResolver;
+
+ private static final String[] PROJECTION = {
+ ContactMethods._ID, // 0
+ ContactMethods.NAME, // 1
+ ContactMethods.DATA // 2
+ };
+
+ public EmailAddressAdapter(Context context) {
+ super(context, android.R.layout.simple_dropdown_item_1line, null);
+ mContentResolver = context.getContentResolver();
+ }
+
+ @Override
+ public final String convertToString(Cursor cursor) {
+ String name = cursor.getString(NAME_INDEX);
+ String address = cursor.getString(DATA_INDEX);
+
+ return new Rfc822Token(name, address, null).toString();
+ }
+
+ private final String makeDisplayString(Cursor cursor) {
+ StringBuilder s = new StringBuilder();
+ boolean flag = false;
+ String name = cursor.getString(NAME_INDEX);
+ String address = cursor.getString(DATA_INDEX);
+
+ if (!TextUtils.isEmpty(name)) {
+ s.append(name);
+ flag = true;
+ }
+
+ if (flag) {
+ s.append(" <");
+ }
+
+ s.append(address);
+
+ if (flag) {
+ s.append(">");
+ }
+
+ return s.toString();
+ }
+
+ @Override
+ public final void bindView(View view, Context context, Cursor cursor) {
+ ((TextView) view).setText(makeDisplayString(cursor));
+ }
+
+ @Override
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ String where = null;
+
+ if (constraint != null) {
+ String filter = DatabaseUtils.sqlEscapeString(constraint.toString() + '%');
+
+ StringBuilder s = new StringBuilder();
+ s.append("(people.name LIKE ");
+ s.append(filter);
+ s.append(") OR (contact_methods.data LIKE ");
+ s.append(filter);
+ s.append(")");
+
+ where = s.toString();
+ }
+
+ return mContentResolver.query(CONTENT_EMAIL_URI, PROJECTION, where, null, SORT_ORDER);
+ }
+}
diff --git a/src/com/android/calendar/EventInfoActivity.java b/src/com/android/calendar/EventInfoActivity.java
index 56eebfe..4c7db60 100644
--- a/src/com/android/calendar/EventInfoActivity.java
+++ b/src/com/android/calendar/EventInfoActivity.java
@@ -21,35 +21,51 @@
import static android.provider.Calendar.AttendeesColumns.ATTENDEE_STATUS;
import android.app.Activity;
+import android.content.AsyncQueryHandler;
+import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
+import android.content.Context;
import android.content.Intent;
+import android.content.OperationApplicationException;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.PorterDuff;
+import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
+import android.os.RemoteException;
+import android.pim.ContactsAsyncHelper;
import android.pim.EventRecurrence;
import android.preference.PreferenceManager;
import android.provider.Calendar;
+import android.provider.Contacts;
import android.provider.Calendar.Attendees;
import android.provider.Calendar.Calendars;
import android.provider.Calendar.Events;
import android.provider.Calendar.Reminders;
+import android.provider.Contacts.ContactMethods;
+import android.provider.Contacts.Intents;
+import android.provider.Contacts.People;
+import android.provider.Contacts.Presence;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.text.util.Linkify;
+import android.text.util.Rfc822Token;
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.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageButton;
+import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
@@ -57,10 +73,12 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.regex.Pattern;
public class EventInfoActivity extends Activity implements View.OnClickListener,
AdapterView.OnItemSelectedListener {
+ private static final String TAG = "EventInfoActivity";
private static final int MAX_REMINDERS = 5;
/**
@@ -100,12 +118,17 @@
private static final String[] ATTENDEES_PROJECTION = new String[] {
Attendees._ID, // 0
- Attendees.ATTENDEE_RELATIONSHIP, // 1
- Attendees.ATTENDEE_STATUS, // 2
+ Attendees.ATTENDEE_NAME, // 1
+ Attendees.ATTENDEE_EMAIL, // 2
+ Attendees.ATTENDEE_RELATIONSHIP, // 3
+ Attendees.ATTENDEE_STATUS, // 4
};
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 int ATTENDEES_INDEX_NAME = 1;
+ private static final int ATTENDEES_INDEX_EMAIL = 2;
+ private static final int ATTENDEES_INDEX_RELATIONSHIP = 3;
+ private static final int ATTENDEES_INDEX_STATUS = 4;
+
private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d";
static final String[] CALENDARS_PROJECTION = new String[] {
@@ -168,6 +191,41 @@
private boolean mIsRepeating;
private Pattern mWildcardPattern = Pattern.compile("^.*$");
+ private LayoutInflater mLayoutInflater;
+
+ private static class ViewHolder {
+ ImageView avatar;
+ ImageView presence;
+ }
+ private HashMap<String, ViewHolder> mPresenceStatuses = new HashMap<String, ViewHolder>();
+ private PresenceQueryHandler mPresenceQueryHandler;
+
+ static final String[] PEOPLE_PROJECTION = new String[] {
+ People._ID,
+ };
+
+ Uri CONTACT_PRESENCE_URI = Uri.withAppendedPath(Contacts.ContactMethods.CONTENT_URI,
+ "with_presence");
+ int PRESENCE_PROJECTION_EMAIL_INDEX = 1;
+ int PRESENCE_PROJECTION_PRESENCE_INDEX = 2;
+ private static final String[] PRESENCE_PROJECTION = new String[] {
+ ContactMethods._ID, // 0
+ ContactMethods.DATA, // 1
+ People.PRESENCE_STATUS, // 2
+ };
+
+ ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>();
+ ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>();
+ ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>();
+ private OnClickListener contactOnClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ Attendee attendee = (Attendee) v.getTag();
+ Rect rect = new Rect();
+ v.getDrawingRect(rect);
+ showContactInfo(attendee, rect);
+ }
+ };
+ private int mColor;
// This is called when one of the "remove reminder" buttons is selected.
public void onClick(View v) {
@@ -177,27 +235,27 @@
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());
@@ -230,7 +288,6 @@
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;
@@ -279,7 +336,7 @@
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()) {
@@ -301,12 +358,15 @@
public void onClick(View v) {
addReminder();
}
- };
+ };
ImageButton reminderRemoveButton = (ImageButton) findViewById(R.id.reminder_add);
reminderRemoveButton.setOnClickListener(addReminderOnClickListener);
mDeleteEventHelper = new DeleteEventHelper(this, true /* exit when done */);
mEditResponseHelper = new EditResponseHelper(this);
+
+ mPresenceQueryHandler = new PresenceQueryHandler(this, cr);
+ mLayoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@Override
@@ -319,6 +379,7 @@
}
initAttendeesCursor();
initCalendarsCursor();
+ updateResponse();
}
/**
@@ -338,10 +399,47 @@
return false;
}
+ private static class Attendee {
+ String mName;
+ String mEmail;
+
+ Attendee(String name, String email) {
+ mName = name;
+ mEmail = email;
+ }
+ }
+
private void initAttendeesCursor() {
if (mAttendeesCursor != null) {
if (mAttendeesCursor.moveToFirst()) {
+ mAcceptedAttendees.clear();
+ mDeclinedAttendees.clear();
+ mTentativeAttendees.clear();
+
+ /*
+ * TODO: We have been relying on the fact that "our user" appears
+ * in the first row. The right way is to look up the email addr
+ * associated with the calendar and do a match here.
+ */
mRelationship = mAttendeesCursor.getInt(ATTENDEES_INDEX_RELATIONSHIP);
+ do {
+ int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
+ String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME);
+ String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
+ switch(status) {
+ case Attendees.ATTENDEE_STATUS_ACCEPTED:
+ mAcceptedAttendees.add(new Attendee(name, email));
+ break;
+ case Attendees.ATTENDEE_STATUS_DECLINED:
+ mDeclinedAttendees.add(new Attendee(name, email));
+ break;
+ default:
+ mTentativeAttendees.add(new Attendee(name, email));
+ }
+ } while (mAttendeesCursor.moveToNext());
+ mAttendeesCursor.moveToFirst();
+
+ updateAttendees();
}
}
}
@@ -361,8 +459,19 @@
ContentResolver cr = getContentResolver();
ArrayList<Integer> reminderMinutes = EditEvent.reminderItemsToMinutes(mReminderItems,
mReminderValues);
- boolean changed = EditEvent.saveReminders(cr, mEventId, reminderMinutes, mOriginalMinutes,
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(3);
+ boolean changed = EditEvent.saveReminders(ops, mEventId, reminderMinutes, mOriginalMinutes,
false /* no force save */);
+ try {
+ cr.applyBatch(Calendars.CONTENT_URI.getAuthority(), ops);
+ } catch (RemoteException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (OperationApplicationException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
changed |= saveResponse(cr);
if (changed) {
Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show();
@@ -414,7 +523,7 @@
return super.onPrepareOptionsMenu(menu);
}
-
+
private void addReminder() {
// TODO: when adding a new reminder, make it different from the
// last one in the list (if any).
@@ -465,7 +574,7 @@
/**
* 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
*/
@@ -510,7 +619,7 @@
}
return false;
}
-
+
private void updateResponse(ContentResolver cr, long eventId, long attendeeId, int status) {
// Update the "selfAttendeeStatus" field for the event
ContentValues values = new ContentValues();
@@ -522,7 +631,7 @@
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.
@@ -535,13 +644,13 @@
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);
@@ -553,7 +662,7 @@
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
cr.insert(Events.CONTENT_URI, values);
} finally {
@@ -602,17 +711,17 @@
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;
+ mColor = mEventCursor.getInt(EVENT_INDEX_COLOR) & 0xbbffffff;
View calBackground = findViewById(R.id.cal_background);
- calBackground.setBackgroundColor(color);
+ calBackground.setBackgroundColor(mColor);
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);
-
+ title.setTextColor(mColor);
+
+ View divider = findViewById(R.id.divider);
+ divider.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
+
// What
if (eventName != null) {
setTextCommon(R.id.title, eventName);
@@ -687,9 +796,125 @@
} else {
setVisibilityCommon(R.id.calendar_container, View.GONE);
}
+ }
- // Response
- updateResponse();
+ private void updateAttendees() {
+ CharSequence[] entries;
+ entries = getResources().getTextArray(R.array.response_labels2);
+ LinearLayout attendeesLayout = (LinearLayout) findViewById(R.id.attendee_list);
+ attendeesLayout.removeAllViewsInLayout();
+ addAttendeesToLayout(mAcceptedAttendees, attendeesLayout, entries[0]);
+ addAttendeesToLayout(mDeclinedAttendees, attendeesLayout, entries[2]);
+ addAttendeesToLayout(mTentativeAttendees, attendeesLayout, entries[1]);
+ }
+
+ private void addAttendeesToLayout(ArrayList<Attendee> attendees, LinearLayout attendeeList,
+ CharSequence sectionTitle) {
+ if (attendees.size() == 0) {
+ return;
+ }
+
+ ContentResolver cr = getContentResolver();
+ // Yes/No/Maybe Title
+ View titleView = mLayoutInflater.inflate(R.layout.contact_item, null);
+ titleView.findViewById(R.id.avatar).setVisibility(View.GONE);
+ View divider = titleView.findViewById(R.id.separator);
+ divider.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
+
+ TextView title = (TextView) titleView.findViewById(R.id.name);
+ title.setText(getString(R.string.response_label, sectionTitle, attendees.size()));
+ title.setTextAppearance(this, R.style.TextAppearance_EventInfo_Label);
+ attendeeList.addView(titleView);
+
+ // Attendees
+ int numOfAttendees = attendees.size();
+ StringBuilder selection = new StringBuilder(Contacts.ContactMethods.DATA + " IN (");
+ String[] selectionArgs = new String[numOfAttendees];
+
+ for (int i = 0; i < numOfAttendees; ++i) {
+ Attendee attendee = attendees.get(i);
+ selectionArgs[i] = attendee.mEmail;
+
+ View v = mLayoutInflater.inflate(R.layout.contact_item, null);
+ v.setOnClickListener(contactOnClickListener);
+ v.setTag(attendee);
+
+ View separator = v.findViewById(R.id.separator);
+ separator.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
+
+ // Text
+ TextView tv = (TextView) v.findViewById(R.id.name);
+ String name = attendee.mName;
+ if (name == null || name.length() == 0) {
+ name = attendee.mEmail;
+ }
+ tv.setText(name);
+
+ ViewHolder vh = new ViewHolder();
+ vh.avatar = (ImageView) v.findViewById(R.id.avatar);
+ vh.presence = (ImageView) v.findViewById(R.id.presence);
+ mPresenceStatuses.put(attendee.mEmail, vh);
+
+ if (i == 0) {
+ selection.append('?');
+ } else {
+ selection.append(", ?");
+ }
+
+ attendeeList.addView(v);
+ }
+ selection.append(')');
+
+ mPresenceQueryHandler.startQuery(0, attendees, CONTACT_PRESENCE_URI, PRESENCE_PROJECTION,
+ selection.toString(), selectionArgs, null);
+ }
+
+ private class PresenceQueryHandler extends AsyncQueryHandler {
+ Context mContext;
+ ContentResolver mContentResolver;
+
+ public PresenceQueryHandler(Context context, ContentResolver cr) {
+ super(cr);
+ mContentResolver = cr;
+ mContext = context;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ String email = cursor.getString(PRESENCE_PROJECTION_EMAIL_INDEX);
+ ViewHolder vh = mPresenceStatuses.get(email);
+ ImageView presenceView = vh.presence;
+ if (presenceView != null) {
+ int status = cursor.getInt(PRESENCE_PROJECTION_PRESENCE_INDEX);
+ presenceView.setImageResource(Presence.getPresenceIconResourceId(status));
+ presenceView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ ArrayList<Attendee> attendees = (ArrayList<Attendee>) cookie;
+ for (Attendee attendee : attendees) {
+ Uri uri = Uri.withAppendedPath(People.WITH_EMAIL_OR_IM_FILTER_URI, Uri
+ .encode(attendee.mEmail));
+ // TODO Get rid of this query.
+ Cursor personCursor = mContentResolver.query(uri, PEOPLE_PROJECTION, null, null,
+ null);
+ if (personCursor != null) {
+ if (personCursor.moveToFirst()) {
+ Uri personUri = ContentUris.withAppendedId(People.CONTENT_URI, personCursor
+ .getInt(0));
+ ViewHolder vh = mPresenceStatuses.get(attendee.mEmail);
+ if (vh != null) {
+ ContactsAsyncHelper.updateImageViewWithContactPhotoAsync(mContext,
+ vh.avatar, personUri, -1);
+ }
+ }
+ personCursor.close();
+ }
+ }
+ }
}
void updateResponse() {
@@ -751,4 +976,32 @@
}
return;
}
+
+ /**
+ * Taken from com.google.android.gm.HtmlConversationActivity
+ *
+ * Send the intent that shows the Contact info corresponding to the email address.
+ */
+ public void showContactInfo(Attendee attendee, Rect rect) {
+ Uri contactUri = Uri.fromParts("mailto", attendee.mEmail, null);
+
+ Intent contactIntent = new Intent(Contacts.Intents.SHOW_OR_CREATE_CONTACT);
+ contactIntent.setData(contactUri);
+
+ // Pass along full E-mail string for possible create dialog
+ Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null);
+ contactIntent.putExtra(Contacts.Intents.EXTRA_CREATE_DESCRIPTION,
+ sender.toString());
+
+ // Mark target position using on-screen coordinates
+ // TODO uncomment when contacts code is in.
+ // contactIntent.putExtra(Intents.EXTRA_TARGET_RECT, rect);
+
+ // Only provide personal name hint if we have one
+ if (attendee.mName != null && attendee.mName.length() > 0) {
+ contactIntent.putExtra(Intents.Insert.NAME, attendee.mName);
+ }
+
+ startActivity(contactIntent);
+ }
}