OSDN Git Service

am 0b69502f: - b/2067801 Show event organizer in event info - b/2065026 Moved attende...
[android-x86/packages-apps-Calendar.git] / src / com / android / calendar / EventInfoActivity.java
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.calendar;
18
19 import static android.provider.Calendar.EVENT_BEGIN_TIME;
20 import static android.provider.Calendar.EVENT_END_TIME;
21 import static android.provider.Calendar.AttendeesColumns.ATTENDEE_STATUS;
22
23 import android.app.Activity;
24 import android.content.ActivityNotFoundException;
25 import android.content.AsyncQueryHandler;
26 import android.content.ContentProviderOperation;
27 import android.content.ContentResolver;
28 import android.content.ContentUris;
29 import android.content.ContentValues;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.OperationApplicationException;
33 import android.content.SharedPreferences;
34 import android.content.res.Resources;
35 import android.database.Cursor;
36 import android.graphics.PorterDuff;
37 import android.graphics.Rect;
38 import android.net.Uri;
39 import android.os.Bundle;
40 import android.os.RemoteException;
41 import android.pim.ContactsAsyncHelper;
42 import android.pim.EventRecurrence;
43 import android.preference.PreferenceManager;
44 import android.provider.Calendar;
45 import android.provider.ContactsContract;
46 import android.provider.Calendar.Attendees;
47 import android.provider.Calendar.Calendars;
48 import android.provider.Calendar.Events;
49 import android.provider.Calendar.Reminders;
50 import android.provider.ContactsContract.Contacts;
51 import android.provider.ContactsContract.Intents;
52 import android.provider.ContactsContract.Presence;
53 import android.provider.ContactsContract.RawContacts;
54 import android.provider.ContactsContract.CommonDataKinds.Email;
55 import android.text.format.DateFormat;
56 import android.text.format.DateUtils;
57 import android.text.format.Time;
58 import android.text.util.Linkify;
59 import android.text.util.Rfc822Token;
60 import android.util.Log;
61 import android.view.KeyEvent;
62 import android.view.LayoutInflater;
63 import android.view.Menu;
64 import android.view.MenuItem;
65 import android.view.MotionEvent;
66 import android.view.View;
67 import android.view.View.OnClickListener;
68 import android.view.View.OnTouchListener;
69 import android.widget.AdapterView;
70 import android.widget.ArrayAdapter;
71 import android.widget.ImageButton;
72 import android.widget.ImageView;
73 import android.widget.LinearLayout;
74 import android.widget.Spinner;
75 import android.widget.TextView;
76 import android.widget.Toast;
77
78 import java.util.ArrayList;
79 import java.util.Arrays;
80 import java.util.HashMap;
81 import java.util.TimeZone;
82 import java.util.regex.Pattern;
83
84 public class EventInfoActivity extends Activity implements View.OnClickListener,
85         AdapterView.OnItemSelectedListener {
86     public static final boolean DEBUG = false;
87
88     public static final String TAG = "EventInfoActivity";
89
90     private static final int MAX_REMINDERS = 5;
91
92     /**
93      * These are the corresponding indices into the array of strings
94      * "R.array.change_response_labels" in the resource file.
95      */
96     static final int UPDATE_SINGLE = 0;
97     static final int UPDATE_ALL = 1;
98
99     private static final String[] EVENT_PROJECTION = new String[] {
100         Events._ID,                  // 0  do not remove; used in DeleteEventHelper
101         Events.TITLE,                // 1  do not remove; used in DeleteEventHelper
102         Events.RRULE,                // 2  do not remove; used in DeleteEventHelper
103         Events.ALL_DAY,              // 3  do not remove; used in DeleteEventHelper
104         Events.CALENDAR_ID,          // 4  do not remove; used in DeleteEventHelper
105         Events.DTSTART,              // 5  do not remove; used in DeleteEventHelper
106         Events._SYNC_ID,             // 6  do not remove; used in DeleteEventHelper
107         Events.EVENT_TIMEZONE,       // 7  do not remove; used in DeleteEventHelper
108         Events.DESCRIPTION,          // 8
109         Events.EVENT_LOCATION,       // 9
110         Events.HAS_ALARM,            // 10
111         Events.ACCESS_LEVEL,         // 11
112         Events.COLOR,                // 12
113         Events.GUESTS_CAN_MODIFY,    // 13
114         // TODO Events.CAN_INVITE_OTHERS is broken. Investigate
115         Events.GUESTS_CAN_INVITE_OTHERS, // 14
116         Events.ORGANIZER,            // 15
117     };
118     private static final int EVENT_INDEX_ID = 0;
119     private static final int EVENT_INDEX_TITLE = 1;
120     private static final int EVENT_INDEX_RRULE = 2;
121     private static final int EVENT_INDEX_ALL_DAY = 3;
122     private static final int EVENT_INDEX_CALENDAR_ID = 4;
123     private static final int EVENT_INDEX_SYNC_ID = 6;
124     private static final int EVENT_INDEX_EVENT_TIMEZONE = 7;
125     private static final int EVENT_INDEX_DESCRIPTION = 8;
126     private static final int EVENT_INDEX_EVENT_LOCATION = 9;
127     private static final int EVENT_INDEX_HAS_ALARM = 10;
128     private static final int EVENT_INDEX_ACCESS_LEVEL = 11;
129     private static final int EVENT_INDEX_COLOR = 12;
130     private static final int EVENT_INDEX_GUESTS_CAN_MODIFY = 13;
131     private static final int EVENT_INDEX_CAN_INVITE_OTHERS = 14;
132     private static final int EVENT_INDEX_ORGANIZER = 15;
133
134     private static final String[] ATTENDEES_PROJECTION = new String[] {
135         Attendees._ID,                      // 0
136         Attendees.ATTENDEE_NAME,            // 1
137         Attendees.ATTENDEE_EMAIL,           // 2
138         Attendees.ATTENDEE_RELATIONSHIP,    // 3
139         Attendees.ATTENDEE_STATUS,          // 4
140     };
141     private static final int ATTENDEES_INDEX_ID = 0;
142     private static final int ATTENDEES_INDEX_NAME = 1;
143     private static final int ATTENDEES_INDEX_EMAIL = 2;
144     private static final int ATTENDEES_INDEX_RELATIONSHIP = 3;
145     private static final int ATTENDEES_INDEX_STATUS = 4;
146
147     private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d";
148
149     private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, "
150             + Attendees.ATTENDEE_EMAIL + " ASC";
151
152     static final String[] CALENDARS_PROJECTION = new String[] {
153         Calendars._ID,           // 0
154         Calendars.DISPLAY_NAME,  // 1
155         Calendars.OWNER_ACCOUNT, // 2
156     };
157     static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
158     static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
159
160     static final String CALENDARS_WHERE = Calendars._ID + "=%d";
161
162     private static final String[] REMINDERS_PROJECTION = new String[] {
163         Reminders._ID,      // 0
164         Reminders.MINUTES,  // 1
165     };
166     private static final int REMINDERS_INDEX_MINUTES = 1;
167     private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=%d AND (" +
168             Reminders.METHOD + "=" + Reminders.METHOD_ALERT + " OR " + Reminders.METHOD + "=" +
169             Reminders.METHOD_DEFAULT + ")";
170     private static final String REMINDERS_SORT = Reminders.MINUTES;
171
172     private static final int MENU_GROUP_REMINDER = 1;
173     private static final int MENU_GROUP_EDIT = 2;
174     private static final int MENU_GROUP_DELETE = 3;
175
176     private static final int MENU_ADD_REMINDER = 1;
177     private static final int MENU_EDIT = 2;
178     private static final int MENU_DELETE = 3;
179
180     private static final int ATTENDEE_NO_RESPONSE = -1;
181     private static final int[] ATTENDEE_VALUES = {
182             ATTENDEE_NO_RESPONSE,
183             Attendees.ATTENDEE_STATUS_ACCEPTED,
184             Attendees.ATTENDEE_STATUS_TENTATIVE,
185             Attendees.ATTENDEE_STATUS_DECLINED,
186     };
187
188     private LinearLayout mRemindersContainer;
189     private LinearLayout mOrganizerContainer;
190     private TextView mOrganizerView;
191
192     private Uri mUri;
193     private long mEventId;
194     private Cursor mEventCursor;
195     private Cursor mAttendeesCursor;
196     private Cursor mCalendarsCursor;
197
198     private long mStartMillis;
199     private long mEndMillis;
200
201     private long mCalendarOwnerAttendeeId = -1;
202     private String mCalendarOwnerAccount;
203     private boolean mCanModifyCalendar;
204     private boolean mIsBusyFreeCalendar;
205     private boolean mCanModifyEvent;
206     private int mNumOfAttendees;
207     private String mOrganizer;
208
209     private ArrayList<Integer> mOriginalMinutes = new ArrayList<Integer>();
210     private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0);
211     private ArrayList<Integer> mReminderValues;
212     private ArrayList<String> mReminderLabels;
213     private int mDefaultReminderMinutes;
214
215     private DeleteEventHelper mDeleteEventHelper;
216     private EditResponseHelper mEditResponseHelper;
217
218     private int mResponseOffset;
219     private int mOriginalAttendeeResponse;
220     private int mAttendeeResponseFromIntent = ATTENDEE_NO_RESPONSE;
221     private boolean mIsRepeating;
222
223     private Pattern mWildcardPattern = Pattern.compile("^.*$");
224     private LayoutInflater mLayoutInflater;
225     private LinearLayout mReminderAdder;
226
227     // TODO This can be removed when the contacts content provider doesn't return duplicates
228     private int mUpdateCounts;
229     private static class ViewHolder {
230         ImageView avatar;
231         ImageView presence;
232         int updateCounts;
233     }
234     private HashMap<String, ViewHolder> mViewHolders = new HashMap<String, ViewHolder>();
235     private PresenceQueryHandler mPresenceQueryHandler;
236
237     private static final Uri CONTACT_DATA_WITH_PRESENCE_URI =
238             Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "data_with_presence");
239
240     int PRESENCE_PROJECTION_CONTACT_ID_INDEX = 0;
241     int PRESENCE_PROJECTION_PRESENCE_INDEX = 1;
242     int PRESENCE_PROJECTION_EMAIL_INDEX = 2;
243     int PRESENCE_PROJECTION_PHOTO_ID_INDEX = 3;
244
245     private static final String[] PRESENCE_PROJECTION = new String[] {
246         RawContacts.CONTACT_ID,   // 0
247         Presence.PRESENCE_STATUS, // 1
248         Email.DATA,               // 2
249         Contacts.PHOTO_ID,        // 3
250     };
251
252     ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>();
253     ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>();
254     ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>();
255     private OnClickListener contactOnClickListener = new OnClickListener() {
256         private Rect getTargetRect(View anchor) {
257             final int[] location = new int[2];
258             anchor.getLocationOnScreen(location);
259
260             final Rect rect = new Rect();
261             rect.left = location[0];
262             rect.top = location[1];
263             rect.right = rect.left + anchor.getWidth();
264             rect.bottom = rect.top + anchor.getHeight();
265             return rect;
266         }
267
268         public void onClick(View avatar) {
269             final Rect target = getTargetRect(avatar);
270             View attendeeItem = (View) avatar.getParent();
271             showContactInfo((Attendee) attendeeItem.getTag(), target);
272         }
273     };
274     private int mColor;
275
276     // This is called when one of the "remove reminder" buttons is selected.
277     public void onClick(View v) {
278         LinearLayout reminderItem = (LinearLayout) v.getParent();
279         LinearLayout parent = (LinearLayout) reminderItem.getParent();
280         parent.removeView(reminderItem);
281         mReminderItems.remove(reminderItem);
282         updateRemindersVisibility();
283     }
284
285     public void onItemSelected(AdapterView<?> parent, View v, int position, long id) {
286         // If they selected the "No response" option, then don't display the
287         // dialog asking which events to change.
288         if (id == 0 && mResponseOffset == 0) {
289             return;
290         }
291
292         // If this is not a repeating event, then don't display the dialog
293         // asking which events to change.
294         if (!mIsRepeating) {
295             return;
296         }
297
298         // If the selection is the same as the original, then don't display the
299         // dialog asking which events to change.
300         int index = findResponseIndexFor(mOriginalAttendeeResponse);
301         if (position == index + mResponseOffset) {
302             return;
303         }
304
305         // This is a repeating event. We need to ask the user if they mean to
306         // change just this one instance or all instances.
307         mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents());
308     }
309
310     public void onNothingSelected(AdapterView<?> parent) {
311     }
312
313     @Override
314     protected void onCreate(Bundle icicle) {
315         super.onCreate(icicle);
316
317         // Event cursor
318         Intent intent = getIntent();
319         mUri = intent.getData();
320         ContentResolver cr = getContentResolver();
321         mStartMillis = intent.getLongExtra(EVENT_BEGIN_TIME, 0);
322         mEndMillis = intent.getLongExtra(EVENT_END_TIME, 0);
323         mAttendeeResponseFromIntent = intent.getIntExtra(ATTENDEE_STATUS, ATTENDEE_NO_RESPONSE);
324         mEventCursor = managedQuery(mUri, EVENT_PROJECTION, null, null);
325         if (initEventCursor()) {
326             // The cursor is empty. This can happen if the event was deleted.
327             finish();
328             return;
329         }
330
331         setContentView(R.layout.event_info_activity);
332
333         // Attendees cursor
334         Uri uri = Attendees.CONTENT_URI;
335         String where = String.format(ATTENDEES_WHERE, mEventId);
336         mAttendeesCursor = managedQuery(uri, ATTENDEES_PROJECTION, where, ATTENDEES_SORT_ORDER);
337
338         // Calendars cursor
339         uri = Calendars.CONTENT_URI;
340         where = String.format(CALENDARS_WHERE, mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID));
341         mCalendarsCursor = managedQuery(uri, CALENDARS_PROJECTION, where, null);
342         mCalendarOwnerAccount = "";
343         if (mCalendarsCursor != null) {
344             mCalendarsCursor.moveToFirst();
345             mCalendarOwnerAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
346         }
347
348         String eventOrganizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER);
349         mOrganizer = eventOrganizer;
350         mCanModifyCalendar =
351                 mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) >= Calendars.CONTRIBUTOR_ACCESS;
352         mIsBusyFreeCalendar =
353                 mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.FREEBUSY_ACCESS;
354         mCanModifyEvent = mCanModifyCalendar
355                 && (mCalendarOwnerAccount.equals(eventOrganizer)
356                         || mEventCursor.getInt(EVENT_INDEX_GUESTS_CAN_MODIFY) != 0);
357
358         // Initialize the reminder values array.
359         Resources r = getResources();
360         String[] strings = r.getStringArray(R.array.reminder_minutes_values);
361         int size = strings.length;
362         ArrayList<Integer> list = new ArrayList<Integer>(size);
363         for (int i = 0 ; i < size ; i++) {
364             list.add(Integer.parseInt(strings[i]));
365         }
366         mReminderValues = list;
367         String[] labels = r.getStringArray(R.array.reminder_minutes_labels);
368         mReminderLabels = new ArrayList<String>(Arrays.asList(labels));
369
370         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
371         String durationString =
372                 prefs.getString(CalendarPreferenceActivity.KEY_DEFAULT_REMINDER, "0");
373         mDefaultReminderMinutes = Integer.parseInt(durationString);
374
375         mRemindersContainer = (LinearLayout) findViewById(R.id.reminders_container);
376         mOrganizerContainer = (LinearLayout) findViewById(R.id.organizer_container);
377         mOrganizerView = (TextView) findViewById(R.id.organizer);
378
379         // Reminders cursor
380         boolean hasAlarm = mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0;
381         if (hasAlarm) {
382             uri = Reminders.CONTENT_URI;
383             where = String.format(REMINDERS_WHERE, mEventId);
384             Cursor reminderCursor = cr.query(uri, REMINDERS_PROJECTION, where, null,
385                     REMINDERS_SORT);
386             try {
387                 // First pass: collect all the custom reminder minutes (e.g.,
388                 // a reminder of 8 minutes) into a global list.
389                 while (reminderCursor.moveToNext()) {
390                     int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
391                     EditEvent.addMinutesToList(this, mReminderValues, mReminderLabels, minutes);
392                 }
393
394                 // Second pass: create the reminder spinners
395                 reminderCursor.moveToPosition(-1);
396                 while (reminderCursor.moveToNext()) {
397                     int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
398                     mOriginalMinutes.add(minutes);
399                     EditEvent.addReminder(this, this, mReminderItems, mReminderValues,
400                             mReminderLabels, minutes);
401                 }
402             } finally {
403                 reminderCursor.close();
404             }
405         }
406
407         updateView();
408
409         // Setup the + Add Reminder Button
410         View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
411             public void onClick(View v) {
412                 addReminder();
413             }
414         };
415         ImageButton reminderAddButton = (ImageButton) findViewById(R.id.reminder_add);
416         reminderAddButton.setOnClickListener(addReminderOnClickListener);
417
418         mReminderAdder = (LinearLayout) findViewById(R.id.reminder_adder);
419         updateRemindersVisibility();
420
421         mDeleteEventHelper = new DeleteEventHelper(this, true /* exit when done */);
422         mEditResponseHelper = new EditResponseHelper(this);
423
424         mPresenceQueryHandler = new PresenceQueryHandler(this, cr);
425         mLayoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
426     }
427
428     @Override
429     protected void onResume() {
430         super.onResume();
431         if (initEventCursor()) {
432             // The cursor is empty. This can happen if the event was deleted.
433             finish();
434             return;
435         }
436         initAttendeesCursor();
437         initCalendarsCursor();
438         updateResponse();
439         updateTitle();
440     }
441
442     private void updateTitle() {
443         Resources res = getResources();
444         if (mCanModifyCalendar && mNumOfAttendees > 1) {
445             setTitle(res.getString(R.string.event_info_title_invite));
446         } else {
447             setTitle(res.getString(R.string.event_info_title));
448         }
449     }
450
451     /**
452      * Initializes the event cursor, which is expected to point to the first
453      * (and only) result from a query.
454      * @return true if the cursor is empty.
455      */
456     private boolean initEventCursor() {
457         if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) {
458             return true;
459         }
460         mEventCursor.moveToFirst();
461         mEventId = mEventCursor.getInt(EVENT_INDEX_ID);
462         String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
463         mIsRepeating = (rRule != null);
464         return false;
465     }
466
467     private static class Attendee {
468         String mName;
469         String mEmail;
470
471         Attendee(String name, String email) {
472             mName = name;
473             mEmail = email;
474         }
475     }
476
477     private void initAttendeesCursor() {
478         mOriginalAttendeeResponse = ATTENDEE_NO_RESPONSE;
479         mCalendarOwnerAttendeeId = -1;
480         mNumOfAttendees = 0;
481         if (mAttendeesCursor != null) {
482             mNumOfAttendees = mAttendeesCursor.getCount();
483             if (mAttendeesCursor.moveToFirst()) {
484                 mAcceptedAttendees.clear();
485                 mDeclinedAttendees.clear();
486                 mTentativeAttendees.clear();
487
488                 do {
489                     int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
490                     String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME);
491                     String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
492
493                     if (mAttendeesCursor.getInt(ATTENDEES_INDEX_RELATIONSHIP) ==
494                             Attendees.RELATIONSHIP_ORGANIZER) {
495                         // Overwrites the one from Event table if available
496                         if (name != null && name.length() > 0) {
497                             mOrganizer = name;
498                         } else if (email != null && email.length() > 0) {
499                             mOrganizer = email;
500                         }
501                     }
502
503                     if (mCalendarOwnerAttendeeId == -1 && mCalendarOwnerAccount.equals(email)) {
504                         mCalendarOwnerAttendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID);
505                         mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
506                     }
507
508                     switch(status) {
509                         case Attendees.ATTENDEE_STATUS_ACCEPTED:
510                             mAcceptedAttendees.add(new Attendee(name, email));
511                             break;
512                         case Attendees.ATTENDEE_STATUS_DECLINED:
513                             mDeclinedAttendees.add(new Attendee(name, email));
514                             break;
515                         default:
516                             mTentativeAttendees.add(new Attendee(name, email));
517                     }
518                 } while (mAttendeesCursor.moveToNext());
519                 mAttendeesCursor.moveToFirst();
520
521                 updateAttendees();
522             }
523         }
524         if (mNumOfAttendees > 1) {
525             mOrganizerContainer.setVisibility(View.VISIBLE);
526             mOrganizerView.setText(mOrganizer);
527         } else {
528             mOrganizerContainer.setVisibility(View.GONE);
529         }
530     }
531
532     private void initCalendarsCursor() {
533         if (mCalendarsCursor != null) {
534             mCalendarsCursor.moveToFirst();
535         }
536     }
537
538     @Override
539     public void onPause() {
540         super.onPause();
541         if (!isFinishing()) {
542             return;
543         }
544         ContentResolver cr = getContentResolver();
545         ArrayList<Integer> reminderMinutes = EditEvent.reminderItemsToMinutes(mReminderItems,
546                 mReminderValues);
547         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(3);
548         boolean changed = EditEvent.saveReminders(ops, mEventId, reminderMinutes, mOriginalMinutes,
549                 false /* no force save */);
550         try {
551             cr.applyBatch(Calendars.CONTENT_URI.getAuthority(), ops);
552         } catch (RemoteException e) {
553             // TODO Auto-generated catch block
554             e.printStackTrace();
555         } catch (OperationApplicationException e) {
556             // TODO Auto-generated catch block
557             e.printStackTrace();
558         }
559
560         changed |= saveResponse(cr);
561         if (changed) {
562             Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show();
563         }
564     }
565
566     @Override
567     public boolean onCreateOptionsMenu(Menu menu) {
568         MenuItem item;
569         item = menu.add(MENU_GROUP_REMINDER, MENU_ADD_REMINDER, 0,
570                 R.string.add_new_reminder);
571         item.setIcon(R.drawable.ic_menu_reminder);
572         item.setAlphabeticShortcut('r');
573
574         item = menu.add(MENU_GROUP_EDIT, MENU_EDIT, 0, R.string.edit_event_label);
575         item.setIcon(android.R.drawable.ic_menu_edit);
576         item.setAlphabeticShortcut('e');
577
578         item = menu.add(MENU_GROUP_DELETE, MENU_DELETE, 0, R.string.delete_event_label);
579         item.setIcon(android.R.drawable.ic_menu_delete);
580
581         return super.onCreateOptionsMenu(menu);
582     }
583
584     @Override
585     public boolean onPrepareOptionsMenu(Menu menu) {
586         boolean canAddReminders = canAddReminders();
587         menu.setGroupVisible(MENU_GROUP_REMINDER, canAddReminders);
588         menu.setGroupEnabled(MENU_GROUP_REMINDER, canAddReminders);
589
590         menu.setGroupVisible(MENU_GROUP_EDIT, mCanModifyEvent);
591         menu.setGroupEnabled(MENU_GROUP_EDIT, mCanModifyEvent);
592         menu.setGroupVisible(MENU_GROUP_DELETE, mCanModifyCalendar);
593         menu.setGroupEnabled(MENU_GROUP_DELETE, mCanModifyCalendar);
594
595         return super.onPrepareOptionsMenu(menu);
596     }
597
598     private boolean canAddReminders() {
599         return !mIsBusyFreeCalendar && mReminderItems.size() < MAX_REMINDERS;
600     }
601
602     private void addReminder() {
603         // TODO: when adding a new reminder, make it different from the
604         // last one in the list (if any).
605         if (mDefaultReminderMinutes == 0) {
606             EditEvent.addReminder(this, this, mReminderItems,
607                     mReminderValues, mReminderLabels, 10 /* minutes */);
608         } else {
609             EditEvent.addReminder(this, this, mReminderItems,
610                     mReminderValues, mReminderLabels, mDefaultReminderMinutes);
611         }
612         updateRemindersVisibility();
613     }
614
615     @Override
616     public boolean onOptionsItemSelected(MenuItem item) {
617         super.onOptionsItemSelected(item);
618         switch (item.getItemId()) {
619         case MENU_ADD_REMINDER:
620             addReminder();
621             break;
622         case MENU_EDIT:
623             doEdit();
624             break;
625         case MENU_DELETE:
626             doDelete();
627             break;
628         }
629         return true;
630     }
631
632     @Override
633     public boolean onKeyDown(int keyCode, KeyEvent event) {
634         if (keyCode == KeyEvent.KEYCODE_DEL) {
635             doDelete();
636             return true;
637         }
638         return super.onKeyDown(keyCode, event);
639     }
640
641     private void updateRemindersVisibility() {
642         if (mIsBusyFreeCalendar) {
643             mRemindersContainer.setVisibility(View.GONE);
644         } else {
645             mRemindersContainer.setVisibility(View.VISIBLE);
646             mReminderAdder.setVisibility(canAddReminders() ? View.VISIBLE : View.GONE);
647         }
648     }
649
650     /**
651      * Saves the response to an invitation if the user changed the response.
652      * Returns true if the database was updated.
653      *
654      * @param cr the ContentResolver
655      * @return true if the database was changed
656      */
657     private boolean saveResponse(ContentResolver cr) {
658         if (mAttendeesCursor == null || mEventCursor == null) {
659             return false;
660         }
661         Spinner spinner = (Spinner) findViewById(R.id.response_value);
662         int position = spinner.getSelectedItemPosition() - mResponseOffset;
663         if (position <= 0) {
664             return false;
665         }
666
667         int status = ATTENDEE_VALUES[position];
668
669         // If the status has not changed, then don't update the database
670         if (status == mOriginalAttendeeResponse) {
671             return false;
672         }
673
674         if (!mIsRepeating) {
675             // This is a non-repeating event
676             updateResponse(cr, mEventId, mCalendarOwnerAttendeeId, status);
677             return true;
678         }
679
680         // This is a repeating event
681         int whichEvents = mEditResponseHelper.getWhichEvents();
682         switch (whichEvents) {
683             case -1:
684                 return false;
685             case UPDATE_SINGLE:
686                 createExceptionResponse(cr, mEventId, mCalendarOwnerAttendeeId, status);
687                 return true;
688             case UPDATE_ALL:
689                 updateResponse(cr, mEventId, mCalendarOwnerAttendeeId, status);
690                 return true;
691             default:
692                 Log.e("Calendar", "Unexpected choice for updating invitation response");
693                 break;
694         }
695         return false;
696     }
697
698     private void updateResponse(ContentResolver cr, long eventId, long attendeeId, int status) {
699         // Update the "selfAttendeeStatus" field for the event
700         ContentValues values = new ContentValues();
701
702         // Will need to add email when MULTIPLE_ATTENDEES_PER_EVENT supported.
703         values.put(Attendees.ATTENDEE_STATUS, status);
704         values.put(Attendees.EVENT_ID, eventId);
705
706         Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, attendeeId);
707         cr.update(uri, values, null /* where */, null /* selection args */);
708     }
709
710     private void createExceptionResponse(ContentResolver cr, long eventId,
711             long attendeeId, int status) {
712         // Fetch information about the repeating event.
713         Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
714         Cursor cursor = cr.query(uri, EVENT_PROJECTION, null, null, null);
715         if (cursor == null) {
716             return;
717         }
718
719         try {
720             cursor.moveToFirst();
721             ContentValues values = new ContentValues();
722
723             String title = cursor.getString(EVENT_INDEX_TITLE);
724             String timezone = cursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
725             int calendarId = cursor.getInt(EVENT_INDEX_CALENDAR_ID);
726             boolean allDay = cursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
727             String syncId = cursor.getString(EVENT_INDEX_SYNC_ID);
728
729             values.put(Events.TITLE, title);
730             values.put(Events.EVENT_TIMEZONE, timezone);
731             values.put(Events.ALL_DAY, allDay ? 1 : 0);
732             values.put(Events.CALENDAR_ID, calendarId);
733             values.put(Events.DTSTART, mStartMillis);
734             values.put(Events.DTEND, mEndMillis);
735             values.put(Events.ORIGINAL_EVENT, syncId);
736             values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
737             values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
738             values.put(Events.STATUS, Events.STATUS_CONFIRMED);
739             values.put(Events.SELF_ATTENDEE_STATUS, status);
740
741             // Create a recurrence exception
742             cr.insert(Events.CONTENT_URI, values);
743         } finally {
744             cursor.close();
745         }
746     }
747
748     private int findResponseIndexFor(int response) {
749         int size = ATTENDEE_VALUES.length;
750         for (int index = 0; index < size; index++) {
751             if (ATTENDEE_VALUES[index] == response) {
752                 return index;
753             }
754         }
755         return 0;
756     }
757
758     private void doEdit() {
759         Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
760         Intent intent = new Intent(Intent.ACTION_EDIT, uri);
761         intent.putExtra(Calendar.EVENT_BEGIN_TIME, mStartMillis);
762         intent.putExtra(Calendar.EVENT_END_TIME, mEndMillis);
763         intent.setClass(EventInfoActivity.this, EditEvent.class);
764         startActivity(intent);
765         finish();
766     }
767
768     private void doDelete() {
769         mDeleteEventHelper.delete(mStartMillis, mEndMillis, mEventCursor, -1);
770     }
771
772     private void updateView() {
773         if (mEventCursor == null) {
774             return;
775         }
776         Resources res = getResources();
777
778         String eventName = mEventCursor.getString(EVENT_INDEX_TITLE);
779         if (eventName == null || eventName.length() == 0) {
780             eventName = res.getString(R.string.no_title_label);
781         }
782
783         boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
784         String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION);
785         String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION);
786         String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
787         boolean hasAlarm = mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0;
788         String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
789         mColor = mEventCursor.getInt(EVENT_INDEX_COLOR) & 0xbbffffff;
790
791         View calBackground = findViewById(R.id.cal_background);
792         calBackground.setBackgroundColor(mColor);
793
794         TextView title = (TextView) findViewById(R.id.title);
795         title.setTextColor(mColor);
796
797         View divider = findViewById(R.id.divider);
798         divider.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
799
800         // What
801         if (eventName != null) {
802             setTextCommon(R.id.title, eventName);
803         }
804
805         // When
806         String when;
807         int flags;
808         if (allDay) {
809             flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE;
810         } else {
811             flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE;
812             if (DateFormat.is24HourFormat(this)) {
813                 flags |= DateUtils.FORMAT_24HOUR;
814             }
815         }
816         when = DateUtils.formatDateRange(this, mStartMillis, mEndMillis, flags);
817         setTextCommon(R.id.when, when);
818
819         // Show the event timezone if it is different from the local timezone
820         Time time = new Time();
821         String localTimezone = time.timezone;
822         if (allDay) {
823             localTimezone = Time.TIMEZONE_UTC;
824         }
825         if (eventTimezone != null && !localTimezone.equals(eventTimezone) && !allDay) {
826             String displayName;
827             TimeZone tz = TimeZone.getTimeZone(localTimezone);
828             if (tz == null || tz.getID().equals("GMT")) {
829                 displayName = localTimezone;
830             } else {
831                 displayName = tz.getDisplayName();
832             }
833
834             setTextCommon(R.id.timezone, displayName);
835         } else {
836             setVisibilityCommon(R.id.timezone_container, View.GONE);
837         }
838
839         // Repeat
840         if (rRule != null) {
841             EventRecurrence eventRecurrence = new EventRecurrence();
842             eventRecurrence.parse(rRule);
843             Time date = new Time();
844             if (allDay) {
845                 date.timezone = Time.TIMEZONE_UTC;
846             }
847             date.set(mStartMillis);
848             eventRecurrence.setStartDate(date);
849             String repeatString = eventRecurrence.getRepeatString();
850             setTextCommon(R.id.repeat, repeatString);
851         } else {
852             setVisibilityCommon(R.id.repeat_container, View.GONE);
853         }
854
855         // Where
856         if (location == null || location.length() == 0) {
857             setVisibilityCommon(R.id.where, View.GONE);
858         } else {
859             final TextView textView = (TextView) findViewById(R.id.where);
860             if (textView != null) {
861                     textView.setAutoLinkMask(0);
862                     textView.setText(location);
863                     Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q=");
864                     textView.setOnTouchListener(new OnTouchListener() {
865                         public boolean onTouch(View v, MotionEvent event) {
866                             try {
867                                 return v.onTouchEvent(event);
868                             } catch (ActivityNotFoundException e) {
869                                 // ignore
870                                 return true;
871                             }
872                         }
873                     });
874             }
875         }
876
877         // Description
878         if (description == null || description.length() == 0) {
879             setVisibilityCommon(R.id.description, View.GONE);
880         } else {
881             setTextCommon(R.id.description, description);
882         }
883
884         // Calendar
885         if (mCalendarsCursor != null) {
886             String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
887             setTextCommon(R.id.calendar, calendarName);
888         } else {
889             setVisibilityCommon(R.id.calendar_container, View.GONE);
890         }
891     }
892
893     private void updateAttendees() {
894         CharSequence[] entries;
895         entries = getResources().getTextArray(R.array.response_labels2);
896         LinearLayout attendeesLayout = (LinearLayout) findViewById(R.id.attendee_list);
897         attendeesLayout.removeAllViewsInLayout();
898         ++mUpdateCounts;
899         addAttendeesToLayout(mAcceptedAttendees, attendeesLayout, entries[0]);
900         addAttendeesToLayout(mDeclinedAttendees, attendeesLayout, entries[2]);
901         addAttendeesToLayout(mTentativeAttendees, attendeesLayout, entries[1]);
902     }
903
904     private void addAttendeesToLayout(ArrayList<Attendee> attendees, LinearLayout attendeeList,
905             CharSequence sectionTitle) {
906         if (attendees.size() == 0) {
907             return;
908         }
909
910         ContentResolver cr = getContentResolver();
911         // Yes/No/Maybe Title
912         View titleView = mLayoutInflater.inflate(R.layout.contact_item, null);
913         titleView.findViewById(R.id.avatar).setVisibility(View.GONE);
914         View divider = titleView.findViewById(R.id.separator);
915         divider.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
916
917         TextView title = (TextView) titleView.findViewById(R.id.name);
918         title.setText(getString(R.string.response_label, sectionTitle, attendees.size()));
919         title.setTextAppearance(this, R.style.TextAppearance_EventInfo_Label);
920         attendeeList.addView(titleView);
921
922         // Attendees
923         int numOfAttendees = attendees.size();
924         StringBuilder selection = new StringBuilder(Email.DATA + " IN (");
925         String[] selectionArgs = new String[numOfAttendees];
926
927         for (int i = 0; i < numOfAttendees; ++i) {
928             Attendee attendee = attendees.get(i);
929             selectionArgs[i] = attendee.mEmail;
930
931             View v = mLayoutInflater.inflate(R.layout.contact_item, null);
932             v.setTag(attendee);
933
934             View separator = v.findViewById(R.id.separator);
935             separator.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
936
937             // Text
938             TextView tv = (TextView) v.findViewById(R.id.name);
939             String name = attendee.mName;
940             if (name == null || name.length() == 0) {
941                 name = attendee.mEmail;
942             }
943             tv.setText(name);
944
945             ViewHolder vh = new ViewHolder();
946             vh.avatar = (ImageView) v.findViewById(R.id.avatar);
947             vh.avatar.setOnClickListener(contactOnClickListener);
948             vh.presence = (ImageView) v.findViewById(R.id.presence);
949             mViewHolders.put(attendee.mEmail, vh);
950
951             if (i == 0) {
952                 selection.append('?');
953             } else {
954                 selection.append(", ?");
955             }
956
957             attendeeList.addView(v);
958         }
959         selection.append(')');
960
961         mPresenceQueryHandler.startQuery(mUpdateCounts, attendees, CONTACT_DATA_WITH_PRESENCE_URI,
962                 PRESENCE_PROJECTION, selection.toString(), selectionArgs, null);
963     }
964
965     private class PresenceQueryHandler extends AsyncQueryHandler {
966         Context mContext;
967         ContentResolver mContentResolver;
968
969         public PresenceQueryHandler(Context context, ContentResolver cr) {
970             super(cr);
971             mContentResolver = cr;
972             mContext = context;
973         }
974
975         @Override
976         protected void onQueryComplete(int queryIndex, Object cookie, Cursor cursor) {
977             if (cursor == null) {
978                 if (DEBUG) {
979                     Log.e(TAG, "onQueryComplete: cursor == null");
980                 }
981                 return;
982             }
983
984             cursor.moveToPosition(-1);
985             while (cursor.moveToNext()) {
986                 String email = cursor.getString(PRESENCE_PROJECTION_EMAIL_INDEX);
987                 int contactId = cursor.getInt(PRESENCE_PROJECTION_CONTACT_ID_INDEX);
988                 ViewHolder vh = mViewHolders.get(email);
989                 int photoId = cursor.getInt(PRESENCE_PROJECTION_PHOTO_ID_INDEX);
990                 if (DEBUG) {
991                     Log.e(TAG, "onQueryComplete Id: " + contactId + " PhotoId: " + photoId
992                             + " Email: " + email);
993                 }
994                 if (vh == null) {
995                     continue;
996                 }
997                 ImageView presenceView = vh.presence;
998                 if (presenceView != null) {
999                     int status = cursor.getInt(PRESENCE_PROJECTION_PRESENCE_INDEX);
1000                     presenceView.setImageResource(Presence.getPresenceIconResourceId(status));
1001                     presenceView.setVisibility(View.VISIBLE);
1002                 }
1003
1004                 if (photoId > 0 && vh.updateCounts < queryIndex) {
1005                     vh.updateCounts = queryIndex;
1006                     Uri personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1007                     ContactsAsyncHelper.updateImageViewWithContactPhotoAsync(mContext, vh.avatar,
1008                             personUri, R.drawable.ic_contact_picture);
1009                 }
1010             }
1011         }
1012     }
1013
1014     void updateResponse() {
1015         if (!mCanModifyCalendar || mNumOfAttendees <= 1) {
1016             setVisibilityCommon(R.id.response_container, View.GONE);
1017             return;
1018         }
1019
1020         setVisibilityCommon(R.id.response_container, View.VISIBLE);
1021
1022         Spinner spinner = (Spinner) findViewById(R.id.response_value);
1023
1024         mResponseOffset = 0;
1025
1026         /* If the user has previously responded to this event
1027          * we should not allow them to select no response again.
1028          * Switch the entries to a set of entries without the
1029          * no response option.
1030          */
1031         if ((mOriginalAttendeeResponse != Attendees.ATTENDEE_STATUS_INVITED)
1032                 && (mOriginalAttendeeResponse != ATTENDEE_NO_RESPONSE)
1033                 && (mOriginalAttendeeResponse != Attendees.ATTENDEE_STATUS_NONE)) {
1034             CharSequence[] entries;
1035             entries = getResources().getTextArray(R.array.response_labels2);
1036             mResponseOffset = -1;
1037             ArrayAdapter<CharSequence> adapter =
1038                 new ArrayAdapter<CharSequence>(this,
1039                         android.R.layout.simple_spinner_item, entries);
1040             adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1041             spinner.setAdapter(adapter);
1042         }
1043
1044         int index;
1045         if (mAttendeeResponseFromIntent != ATTENDEE_NO_RESPONSE) {
1046             index = findResponseIndexFor(mAttendeeResponseFromIntent);
1047         } else {
1048             index = findResponseIndexFor(mOriginalAttendeeResponse);
1049         }
1050         spinner.setSelection(index + mResponseOffset);
1051         spinner.setOnItemSelectedListener(this);
1052     }
1053
1054     private void setTextCommon(int id, CharSequence text) {
1055         TextView textView = (TextView) findViewById(id);
1056         if (textView == null)
1057             return;
1058         textView.setText(text);
1059     }
1060
1061     private void setVisibilityCommon(int id, int visibility) {
1062         View v = findViewById(id);
1063         if (v != null) {
1064             v.setVisibility(visibility);
1065         }
1066         return;
1067     }
1068
1069     /**
1070      * Taken from com.google.android.gm.HtmlConversationActivity
1071      *
1072      * Send the intent that shows the Contact info corresponding to the email address.
1073      */
1074     public void showContactInfo(Attendee attendee, Rect rect) {
1075         Uri contactUri = Uri.fromParts("mailto", attendee.mEmail, null);
1076
1077         Intent contactIntent = new Intent(Intents.SHOW_OR_CREATE_CONTACT);
1078         contactIntent.setData(contactUri);
1079
1080         // Pass along full E-mail string for possible create dialog
1081         Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null);
1082         contactIntent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION,
1083                 sender.toString());
1084
1085         // Mark target position using on-screen coordinates
1086         contactIntent.putExtra(Intents.EXTRA_TARGET_RECT, rect);
1087
1088         // Show the small version of fast track
1089         contactIntent.putExtra(Intents.EXTRA_MODE, Intents.MODE_SMALL);
1090
1091         // Only provide personal name hint if we have one
1092         if (attendee.mName != null && attendee.mName.length() > 0) {
1093             contactIntent.putExtra(Intents.Insert.NAME, attendee.mName);
1094         }
1095
1096         startActivity(contactIntent);
1097     }
1098 }