EventInfoFragment.java revision 4b441bd6544fe6d11be75f974a41afd8fa040a4f
1/*
2 * Copyright (C) 2010 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
17package com.android.calendar;
18
19import com.android.calendar.CalendarController.EventType;
20import com.android.calendar.event.EditEventHelper;
21import com.android.calendar.event.EventViewUtils;
22
23import android.app.Activity;
24import android.app.Dialog;
25import android.app.DialogFragment;
26import android.content.ActivityNotFoundException;
27import android.content.AsyncQueryHandler;
28import android.content.ContentProviderOperation;
29import android.content.ContentResolver;
30import android.content.ContentUris;
31import android.content.ContentValues;
32import android.content.Context;
33import android.content.Intent;
34import android.content.SharedPreferences;
35import android.content.res.Resources;
36import android.database.Cursor;
37import android.database.MatrixCursor;
38import android.graphics.PorterDuff;
39import android.graphics.Rect;
40import android.net.Uri;
41import android.os.Bundle;
42import android.pim.EventRecurrence;
43import android.provider.ContactsContract;
44import android.provider.Calendar.Attendees;
45import android.provider.Calendar.Calendars;
46import android.provider.Calendar.Events;
47import android.provider.Calendar.Reminders;
48import android.provider.ContactsContract.CommonDataKinds;
49import android.provider.ContactsContract.Contacts;
50import android.provider.ContactsContract.Data;
51import android.provider.ContactsContract.Intents;
52import android.provider.ContactsContract.Presence;
53import android.provider.ContactsContract.QuickContact;
54import android.provider.ContactsContract.CommonDataKinds.Email;
55import android.text.Spannable;
56import android.text.SpannableStringBuilder;
57import android.text.TextUtils;
58import android.text.format.DateFormat;
59import android.text.format.DateUtils;
60import android.text.format.Time;
61import android.text.style.ForegroundColorSpan;
62import android.text.style.StrikethroughSpan;
63import android.text.util.Linkify;
64import android.text.util.Rfc822Token;
65import android.util.Log;
66import android.view.Gravity;
67import android.view.LayoutInflater;
68import android.view.Menu;
69import android.view.MenuInflater;
70import android.view.MenuItem;
71import android.view.MotionEvent;
72import android.view.View;
73import android.view.ViewGroup;
74import android.view.Window;
75import android.view.WindowManager;
76import android.view.View.OnClickListener;
77import android.view.View.OnTouchListener;
78import android.widget.AdapterView;
79import android.widget.ArrayAdapter;
80import android.widget.Button;
81import android.widget.ImageButton;
82import android.widget.ImageView;
83import android.widget.LinearLayout;
84import android.widget.QuickContactBadge;
85import android.widget.Spinner;
86import android.widget.TextView;
87import android.widget.Toast;
88
89import java.util.ArrayList;
90import java.util.Arrays;
91import java.util.HashMap;
92import java.util.regex.Pattern;
93
94public class EventInfoFragment extends DialogFragment implements View.OnClickListener,
95        AdapterView.OnItemSelectedListener {
96    public static final boolean DEBUG = false;
97
98    public static final String TAG = "EventInfoActivity";
99
100    private static final String BUNDLE_KEY_EVENT_ID = "key_event_id";
101
102    private static final String BUNDLE_KEY_START_MILLIS = "key_start_millis";
103
104    private static final String BUNDLE_KEY_END_MILLIS = "key_end_millis";
105
106    private static final String BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog";
107
108    private static final int MAX_REMINDERS = 5;
109
110    /**
111     * These are the corresponding indices into the array of strings
112     * "R.array.change_response_labels" in the resource file.
113     */
114    static final int UPDATE_SINGLE = 0;
115    static final int UPDATE_ALL = 1;
116
117    // Query tokens for QueryHandler
118    private static final int TOKEN_QUERY_EVENT = 0;
119    private static final int TOKEN_QUERY_CALENDARS = 1;
120    private static final int TOKEN_QUERY_ATTENDEES = 2;
121    private static final int TOKEN_QUERY_REMINDERS = 3;
122    private static final int TOKEN_QUERY_DUPLICATE_CALENDARS = 4;
123
124    private static final String[] EVENT_PROJECTION = new String[] {
125        Events._ID,                  // 0  do not remove; used in DeleteEventHelper
126        Events.TITLE,                // 1  do not remove; used in DeleteEventHelper
127        Events.RRULE,                // 2  do not remove; used in DeleteEventHelper
128        Events.ALL_DAY,              // 3  do not remove; used in DeleteEventHelper
129        Events.CALENDAR_ID,          // 4  do not remove; used in DeleteEventHelper
130        Events.DTSTART,              // 5  do not remove; used in DeleteEventHelper
131        Events._SYNC_ID,             // 6  do not remove; used in DeleteEventHelper
132        Events.EVENT_TIMEZONE,       // 7  do not remove; used in DeleteEventHelper
133        Events.DESCRIPTION,          // 8
134        Events.EVENT_LOCATION,       // 9
135        Events.HAS_ALARM,            // 10
136        Calendars.ACCESS_LEVEL,      // 11
137        Calendars.COLOR,             // 12
138        Events.HAS_ATTENDEE_DATA,    // 13
139        Events.GUESTS_CAN_MODIFY,    // 14
140        // TODO Events.GUESTS_CAN_INVITE_OTHERS has not been implemented in calendar provider
141        Events.GUESTS_CAN_INVITE_OTHERS, // 15
142        Events.ORGANIZER,            // 16
143        Events.ORIGINAL_EVENT        // 17 do not remove; used in DeleteEventHelper
144    };
145    private static final int EVENT_INDEX_ID = 0;
146    private static final int EVENT_INDEX_TITLE = 1;
147    private static final int EVENT_INDEX_RRULE = 2;
148    private static final int EVENT_INDEX_ALL_DAY = 3;
149    private static final int EVENT_INDEX_CALENDAR_ID = 4;
150    private static final int EVENT_INDEX_SYNC_ID = 6;
151    private static final int EVENT_INDEX_EVENT_TIMEZONE = 7;
152    private static final int EVENT_INDEX_DESCRIPTION = 8;
153    private static final int EVENT_INDEX_EVENT_LOCATION = 9;
154    private static final int EVENT_INDEX_HAS_ALARM = 10;
155    private static final int EVENT_INDEX_ACCESS_LEVEL = 11;
156    private static final int EVENT_INDEX_COLOR = 12;
157    private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 13;
158    private static final int EVENT_INDEX_GUESTS_CAN_MODIFY = 14;
159    private static final int EVENT_INDEX_CAN_INVITE_OTHERS = 15;
160    private static final int EVENT_INDEX_ORGANIZER = 16;
161
162    private static final String[] ATTENDEES_PROJECTION = new String[] {
163        Attendees._ID,                      // 0
164        Attendees.ATTENDEE_NAME,            // 1
165        Attendees.ATTENDEE_EMAIL,           // 2
166        Attendees.ATTENDEE_RELATIONSHIP,    // 3
167        Attendees.ATTENDEE_STATUS,          // 4
168    };
169    private static final int ATTENDEES_INDEX_ID = 0;
170    private static final int ATTENDEES_INDEX_NAME = 1;
171    private static final int ATTENDEES_INDEX_EMAIL = 2;
172    private static final int ATTENDEES_INDEX_RELATIONSHIP = 3;
173    private static final int ATTENDEES_INDEX_STATUS = 4;
174
175    private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?";
176
177    private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, "
178            + Attendees.ATTENDEE_EMAIL + " ASC";
179
180    static final String[] CALENDARS_PROJECTION = new String[] {
181        Calendars._ID,           // 0
182        Calendars.DISPLAY_NAME,  // 1
183        Calendars.OWNER_ACCOUNT, // 2
184        Calendars.ORGANIZER_CAN_RESPOND // 3
185    };
186    static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
187    static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
188    static final int CALENDARS_INDEX_OWNER_CAN_RESPOND = 3;
189
190    static final String CALENDARS_WHERE = Calendars._ID + "=?";
191    static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.DISPLAY_NAME + "=?";
192
193    private static final String[] REMINDERS_PROJECTION = new String[] {
194        Reminders._ID,      // 0
195        Reminders.MINUTES,  // 1
196    };
197    private static final int REMINDERS_INDEX_MINUTES = 1;
198    private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=? AND (" +
199            Reminders.METHOD + "=" + Reminders.METHOD_ALERT + " OR " + Reminders.METHOD + "=" +
200            Reminders.METHOD_DEFAULT + ")";
201    private static final String REMINDERS_SORT = Reminders.MINUTES;
202
203    private static final int MENU_GROUP_REMINDER = 1;
204    private static final int MENU_GROUP_EDIT = 2;
205    private static final int MENU_GROUP_DELETE = 3;
206
207    private static final int MENU_ADD_REMINDER = 1;
208    private static final int MENU_EDIT = 2;
209    private static final int MENU_DELETE = 3;
210
211    private View mView;
212    private LinearLayout mRemindersContainer;
213    private LinearLayout mOrganizerContainer;
214    private TextView mOrganizerView;
215
216    private Uri mUri;
217    private long mEventId;
218    private Cursor mEventCursor;
219    private Cursor mAttendeesCursor;
220    private Cursor mCalendarsCursor;
221
222    private long mStartMillis;
223    private long mEndMillis;
224
225    private boolean mHasAttendeeData;
226    private boolean mIsOrganizer;
227    private long mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE;
228    private boolean mOrganizerCanRespond;
229    private String mCalendarOwnerAccount;
230    private boolean mCanModifyCalendar;
231    private boolean mIsBusyFreeCalendar;
232    private boolean mCanModifyEvent;
233    private int mNumOfAttendees;
234    private String mOrganizer;
235
236    private ArrayList<Integer> mOriginalMinutes = new ArrayList<Integer>();
237    private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0);
238    private ArrayList<Integer> mReminderValues;
239    private ArrayList<String> mReminderLabels;
240    private int mDefaultReminderMinutes;
241    private boolean mOriginalHasAlarm;
242
243    private EditResponseHelper mEditResponseHelper;
244
245    private int mResponseOffset;
246    private int mOriginalAttendeeResponse;
247    private int mAttendeeResponseFromIntent = EditEventHelper.ATTENDEE_NO_RESPONSE;
248    private boolean mIsRepeating;
249    private boolean mIsDuplicateName;
250
251    private Pattern mWildcardPattern = Pattern.compile("^.*$");
252    private LayoutInflater mLayoutInflater;
253    private LinearLayout mReminderAdder;
254
255    // TODO This can be removed when the contacts content provider doesn't return duplicates
256    private int mUpdateCounts;
257    private static class ViewHolder {
258        QuickContactBadge badge;
259        ImageView presence;
260        int updateCounts;
261    }
262    private HashMap<String, ViewHolder> mViewHolders = new HashMap<String, ViewHolder>();
263    private PresenceQueryHandler mPresenceQueryHandler;
264
265    private static final Uri CONTACT_DATA_WITH_PRESENCE_URI = Data.CONTENT_URI;
266
267    int PRESENCE_PROJECTION_CONTACT_ID_INDEX = 0;
268    int PRESENCE_PROJECTION_PRESENCE_INDEX = 1;
269    int PRESENCE_PROJECTION_EMAIL_INDEX = 2;
270    int PRESENCE_PROJECTION_PHOTO_ID_INDEX = 3;
271
272    private static final String[] PRESENCE_PROJECTION = new String[] {
273        Email.CONTACT_ID,           // 0
274        Email.CONTACT_PRESENCE,     // 1
275        Email.DATA,                 // 2
276        Email.PHOTO_ID,             // 3
277    };
278
279    ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>();
280    ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>();
281    ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>();
282    ArrayList<Attendee> mNoResponseAttendees = new ArrayList<Attendee>();
283    private int mColor;
284
285    private QueryHandler mHandler;
286
287    private static final int DIALOG_WIDTH = 500; // FRAG_TODO scale
288    private static final int DIALOG_HEIGHT = 500;
289    private boolean mIsDialog = false;
290    private int mX = -1;
291    private int mY = -1;
292
293    private class QueryHandler extends AsyncQueryService {
294        public QueryHandler(Context context) {
295            super(context);
296        }
297
298        @Override
299        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
300            // if the activity is finishing, then close the cursor and return
301            final Activity activity = getActivity();
302            if (activity == null || activity.isFinishing()) {
303                cursor.close();
304                return;
305            }
306
307            switch (token) {
308            case TOKEN_QUERY_EVENT:
309                mEventCursor = Utils.matrixCursorFromCursor(cursor);
310                if (initEventCursor()) {
311                    // The cursor is empty. This can happen if the event was
312                    // deleted.
313                    // FRAG_TODO we should no longer rely on Activity.finish()
314                    activity.finish();
315                    return;
316                }
317                updateEvent(mView);
318
319                // start calendar query
320                Uri uri = Calendars.CONTENT_URI;
321                String[] args = new String[] {
322                        Long.toString(mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID))};
323                startQuery(TOKEN_QUERY_CALENDARS, null, uri, CALENDARS_PROJECTION,
324                        CALENDARS_WHERE, args, null);
325                break;
326            case TOKEN_QUERY_CALENDARS:
327                mCalendarsCursor = Utils.matrixCursorFromCursor(cursor);
328                updateCalendar(mView);
329                // FRAG_TODO fragments shouldn't set the title anymore
330                updateTitle();
331                // update the action bar since our option set might have changed
332                activity.invalidateOptionsMenu();
333
334                // this is used for both attendees and reminders
335                args = new String[] { Long.toString(mEventId) };
336
337                // start attendees query
338                uri = Attendees.CONTENT_URI;
339                startQuery(TOKEN_QUERY_ATTENDEES, null, uri, ATTENDEES_PROJECTION,
340                        ATTENDEES_WHERE, args, ATTENDEES_SORT_ORDER);
341
342                // start reminders query
343                mOriginalHasAlarm = mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0;
344                if (mOriginalHasAlarm) {
345                    uri = Reminders.CONTENT_URI;
346                    startQuery(TOKEN_QUERY_REMINDERS, null, uri, REMINDERS_PROJECTION,
347                            REMINDERS_WHERE, args, REMINDERS_SORT);
348                } else {
349                    // if no reminders, hide the appropriate fields
350                    updateRemindersVisibility();
351                }
352                break;
353            case TOKEN_QUERY_ATTENDEES:
354                mAttendeesCursor = Utils.matrixCursorFromCursor(cursor);
355                initAttendeesCursor(mView);
356                updateResponse(mView);
357                break;
358            case TOKEN_QUERY_REMINDERS:
359                MatrixCursor reminderCursor = Utils.matrixCursorFromCursor(cursor);
360                try {
361                    // First pass: collect all the custom reminder minutes
362                    // (e.g., a reminder of 8 minutes) into a global list.
363                    while (reminderCursor.moveToNext()) {
364                        int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
365                        EventViewUtils.addMinutesToList(
366                                activity, mReminderValues, mReminderLabels, minutes);
367                    }
368
369                    // Second pass: create the reminder spinners
370                    reminderCursor.moveToPosition(-1);
371                    while (reminderCursor.moveToNext()) {
372                        int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
373                        mOriginalMinutes.add(minutes);
374                        EventViewUtils.addReminder(activity, mRemindersContainer,
375                                EventInfoFragment.this, mReminderItems, mReminderValues,
376                                mReminderLabels, minutes);
377                    }
378                } finally {
379                    updateRemindersVisibility();
380                    reminderCursor.close();
381                }
382                break;
383            case TOKEN_QUERY_DUPLICATE_CALENDARS:
384                mIsDuplicateName = cursor.getCount() > 1;
385                String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
386//CLEANUP                String ownerAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
387//                if (mIsDuplicateName && !calendarName.equalsIgnoreCase(ownerAccount)) {
388//                    Resources res = activity.getResources();
389//                    TextView ownerText = (TextView) mView.findViewById(R.id.owner);
390//                    ownerText.setText(ownerAccount);
391//                    ownerText.setTextColor(res.getColor(R.color.calendar_owner_text_color));
392//                } else {
393//                    setVisibilityCommon(mView, R.id.owner, View.GONE);
394//                }
395                setTextCommon(mView, R.id.calendar, calendarName);
396                break;
397            }
398            cursor.close();
399        }
400
401    }
402
403    public EventInfoFragment() {
404        mUri = null;
405    }
406
407    public EventInfoFragment(Uri uri, long startMillis, long endMillis, int attendeeResponse) {
408        setStyle(DialogFragment.STYLE_NO_TITLE, 0);
409        mUri = uri;
410        mStartMillis = startMillis;
411        mEndMillis = endMillis;
412        mAttendeeResponseFromIntent = attendeeResponse;
413    }
414
415    public EventInfoFragment(long eventId, long startMillis, long endMillis) {
416        this(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
417                startMillis, endMillis, EventInfoActivity.ATTENDEE_NO_RESPONSE);
418        mEventId = eventId;
419    }
420
421    @Override
422    public void onActivityCreated(Bundle savedInstanceState) {
423        super.onActivityCreated(savedInstanceState);
424
425        if (savedInstanceState != null) {
426            mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false);
427        }
428
429        if (mIsDialog) {
430            applyDialogParams();
431        }
432    }
433
434    private void applyDialogParams() {
435        Dialog dialog = getDialog();
436        dialog.setCanceledOnTouchOutside(true);
437
438        Window window = dialog.getWindow();
439        window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
440
441        WindowManager.LayoutParams a = window.getAttributes();
442        a.dimAmount = .4f;
443
444        a.width = DIALOG_WIDTH;
445
446        if (mX != -1 || mY != -1) {
447            a.x = mX - a.width - 64; // FRAG_TODO event sender should return the left edge or a rect
448            a.y = mY - 64; // FRAG_TODO should set height after layout is done
449            a.gravity = Gravity.LEFT | Gravity.TOP;
450        }
451
452        window.setAttributes(a);
453    }
454
455    public void setDialogParams(int x, int y) {
456        mIsDialog = true;
457        mX = x;
458        mY = y;
459    }
460
461    // This is called when one of the "remove reminder" buttons is selected.
462    public void onClick(View v) {
463        LinearLayout reminderItem = (LinearLayout) v.getParent();
464        LinearLayout parent = (LinearLayout) reminderItem.getParent();
465        parent.removeView(reminderItem);
466        mReminderItems.remove(reminderItem);
467        updateRemindersVisibility();
468    }
469
470    public void onItemSelected(AdapterView<?> parent, View v, int position, long id) {
471        // If they selected the "No response" option, then don't display the
472        // dialog asking which events to change.
473        if (id == 0 && mResponseOffset == 0) {
474            return;
475        }
476
477        // If this is not a repeating event, then don't display the dialog
478        // asking which events to change.
479        if (!mIsRepeating) {
480            return;
481        }
482
483        // If the selection is the same as the original, then don't display the
484        // dialog asking which events to change.
485        int index = findResponseIndexFor(mOriginalAttendeeResponse);
486        if (position == index + mResponseOffset) {
487            return;
488        }
489
490        // This is a repeating event. We need to ask the user if they mean to
491        // change just this one instance or all instances.
492        mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents());
493    }
494
495    public void onNothingSelected(AdapterView<?> parent) {
496    }
497
498    @Override
499    public void onAttach(Activity activity) {
500        super.onAttach(activity);
501        mEditResponseHelper = new EditResponseHelper(activity);
502        setHasOptionsMenu(true);
503        mHandler = new QueryHandler(activity);
504        mPresenceQueryHandler = new PresenceQueryHandler(activity, activity.getContentResolver());
505    }
506
507    @Override
508    public View onCreateView(LayoutInflater inflater, ViewGroup container,
509            Bundle savedInstanceState) {
510        mLayoutInflater = inflater;
511        mView = inflater.inflate(R.layout.event_info_activity, null);
512        mRemindersContainer = (LinearLayout) mView.findViewById(R.id.reminders_container);
513        mOrganizerContainer = (LinearLayout) mView.findViewById(R.id.organizer_container);
514        mOrganizerView = (TextView) mView.findViewById(R.id.organizer);
515
516        // Initialize the reminder values array.
517        Resources r = getActivity().getResources();
518        String[] strings = r.getStringArray(R.array.reminder_minutes_values);
519        int size = strings.length;
520        ArrayList<Integer> list = new ArrayList<Integer>(size);
521        for (int i = 0 ; i < size ; i++) {
522            list.add(Integer.parseInt(strings[i]));
523        }
524        mReminderValues = list;
525        String[] labels = r.getStringArray(R.array.reminder_minutes_labels);
526        mReminderLabels = new ArrayList<String>(Arrays.asList(labels));
527
528        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(getActivity());
529        String durationString =
530                prefs.getString(GeneralPreferences.KEY_DEFAULT_REMINDER, "0");
531        mDefaultReminderMinutes = Integer.parseInt(durationString);
532
533        // Setup the + Add Reminder Button
534        View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
535            public void onClick(View v) {
536                addReminder();
537            }
538        };
539        ImageButton reminderAddButton = (ImageButton) mView.findViewById(R.id.reminder_add);
540        reminderAddButton.setOnClickListener(addReminderOnClickListener);
541
542//CLEANUP        mReminderAdder = (LinearLayout) mView.findViewById(R.id.reminder_adder);
543
544        if (mUri == null) {
545            // restore event ID from bundle
546            mEventId = savedInstanceState.getLong(BUNDLE_KEY_EVENT_ID);
547            mUri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
548            mStartMillis = savedInstanceState.getLong(BUNDLE_KEY_START_MILLIS);
549            mEndMillis = savedInstanceState.getLong(BUNDLE_KEY_END_MILLIS);
550        }
551
552        // start loading the data
553        mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION,
554                null, null, null);
555
556        Button b = (Button) mView.findViewById(R.id.done);
557        b.setOnClickListener(new OnClickListener() {
558            @Override
559            public void onClick(View v) {
560                EventInfoFragment.this.dismiss();
561            }});
562
563        return mView;
564    }
565
566    private void updateTitle() {
567        Resources res = getActivity().getResources();
568        if (mCanModifyCalendar && !mIsOrganizer) {
569            getActivity().setTitle(res.getString(R.string.event_info_title_invite));
570        } else {
571            getActivity().setTitle(res.getString(R.string.event_info_title));
572        }
573    }
574
575    /**
576     * Initializes the event cursor, which is expected to point to the first
577     * (and only) result from a query.
578     * @return true if the cursor is empty.
579     */
580    private boolean initEventCursor() {
581        if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) {
582            return true;
583        }
584        mEventCursor.moveToFirst();
585        mEventId = mEventCursor.getInt(EVENT_INDEX_ID);
586        String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
587        mIsRepeating = (rRule != null);
588        return false;
589    }
590
591    private static class Attendee {
592        String mName;
593        String mEmail;
594
595        Attendee(String name, String email) {
596            mName = name;
597            mEmail = email;
598        }
599
600        String getDisplayName() {
601            if (TextUtils.isEmpty(mName)) {
602                return mEmail;
603            } else {
604                return mName;
605            }
606        }
607    }
608
609    @SuppressWarnings("fallthrough")
610    private void initAttendeesCursor(View view) {
611        mOriginalAttendeeResponse = EditEventHelper.ATTENDEE_NO_RESPONSE;
612        mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE;
613        mNumOfAttendees = 0;
614        if (mAttendeesCursor != null) {
615            mNumOfAttendees = mAttendeesCursor.getCount();
616            if (mAttendeesCursor.moveToFirst()) {
617                mAcceptedAttendees.clear();
618                mDeclinedAttendees.clear();
619                mTentativeAttendees.clear();
620                mNoResponseAttendees.clear();
621
622                do {
623                    int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
624                    String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME);
625                    String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
626
627                    if (mAttendeesCursor.getInt(ATTENDEES_INDEX_RELATIONSHIP) ==
628                            Attendees.RELATIONSHIP_ORGANIZER) {
629                        // Overwrites the one from Event table if available
630                        if (name != null && name.length() > 0) {
631                            mOrganizer = name;
632                        } else if (email != null && email.length() > 0) {
633                            mOrganizer = email;
634                        }
635                    }
636
637                    if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE &&
638                            mCalendarOwnerAccount.equalsIgnoreCase(email)) {
639                        mCalendarOwnerAttendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID);
640                        mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
641                    } else {
642                        // Don't show your own status in the list because:
643                        //  1) it doesn't make sense for event without other guests.
644                        //  2) there's a spinner for that for events with guests.
645                        switch(status) {
646                            case Attendees.ATTENDEE_STATUS_ACCEPTED:
647                                mAcceptedAttendees.add(new Attendee(name, email));
648                                break;
649                            case Attendees.ATTENDEE_STATUS_DECLINED:
650                                mDeclinedAttendees.add(new Attendee(name, email));
651                                break;
652                            case Attendees.ATTENDEE_STATUS_TENTATIVE:
653                                mTentativeAttendees.add(new Attendee(name, email));
654                                break;
655                            default:
656                                mNoResponseAttendees.add(new Attendee(name, email));
657                        }
658                    }
659                } while (mAttendeesCursor.moveToNext());
660                mAttendeesCursor.moveToFirst();
661
662                updateAttendees(view);
663            }
664        }
665        // only show the organizer if we're not the organizer and if
666        // we have attendee data (might have been removed by the server
667        // for events with a lot of attendees).
668//CLEANUP        if (!mIsOrganizer && mHasAttendeeData) {
669//            mOrganizerContainer.setVisibility(View.VISIBLE);
670//            mOrganizerView.setText(mOrganizer);
671//        } else {
672//            mOrganizerContainer.setVisibility(View.GONE);
673//        }
674    }
675
676    @Override
677    public void onSaveInstanceState(Bundle outState) {
678        super.onSaveInstanceState(outState);
679        outState.putLong(BUNDLE_KEY_EVENT_ID, mEventId);
680        outState.putLong(BUNDLE_KEY_START_MILLIS, mStartMillis);
681        outState.putLong(BUNDLE_KEY_END_MILLIS, mEndMillis);
682
683        outState.putBoolean(BUNDLE_KEY_IS_DIALOG, mIsDialog);
684    }
685
686
687    @Override
688    public void onDestroyView() {
689        ArrayList<Integer> reminderMinutes = EventViewUtils.reminderItemsToMinutes(mReminderItems,
690                mReminderValues);
691        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(3);
692        boolean changed = EditEventHelper.saveReminders(ops, mEventId, reminderMinutes,
693                mOriginalMinutes, false /* no force save */);
694        mHandler.startBatch(mHandler.getNextToken(), null,
695                Calendars.CONTENT_URI.getAuthority(), ops, Utils.UNDO_DELAY);
696
697        // Update the "hasAlarm" field for the event
698        Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
699        int len = reminderMinutes.size();
700        boolean hasAlarm = len > 0;
701        if (hasAlarm != mOriginalHasAlarm) {
702            ContentValues values = new ContentValues();
703            values.put(Events.HAS_ALARM, hasAlarm ? 1 : 0);
704            mHandler.startUpdate(mHandler.getNextToken(), null, uri, values,
705                    null, null, Utils.UNDO_DELAY);
706        }
707
708        changed |= saveResponse();
709        if (changed) {
710            Toast.makeText(getActivity(), R.string.saving_event, Toast.LENGTH_SHORT).show();
711        }
712        super.onDestroyView();
713    }
714
715    @Override
716    public void onDestroy() {
717        if (mEventCursor != null) {
718            mEventCursor.close();
719        }
720        if (mCalendarsCursor != null) {
721            mCalendarsCursor.close();
722        }
723        if (mAttendeesCursor != null) {
724            mAttendeesCursor.close();
725        }
726        super.onDestroy();
727    }
728
729    private boolean canAddReminders() {
730        return !mIsBusyFreeCalendar && mReminderItems.size() < MAX_REMINDERS;
731    }
732
733    private void addReminder() {
734        // TODO: when adding a new reminder, make it different from the
735        // last one in the list (if any).
736        if (mDefaultReminderMinutes == 0) {
737            EventViewUtils.addReminder(getActivity(), mRemindersContainer, this, mReminderItems,
738                    mReminderValues, mReminderLabels, 10 /* minutes */);
739        } else {
740            EventViewUtils.addReminder(getActivity(), mRemindersContainer, this, mReminderItems,
741                    mReminderValues, mReminderLabels, mDefaultReminderMinutes);
742        }
743        updateRemindersVisibility();
744    }
745
746    @Override
747    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
748        MenuItem item;
749        item = menu.add(MENU_GROUP_REMINDER, MENU_ADD_REMINDER, 0,
750                R.string.add_new_reminder);
751        item.setIcon(R.drawable.ic_menu_reminder);
752        item.setAlphabeticShortcut('r');
753        item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
754
755        item = menu.add(MENU_GROUP_EDIT, MENU_EDIT, 0, R.string.edit_event_label);
756        item.setIcon(android.R.drawable.ic_menu_edit);
757        item.setAlphabeticShortcut('e');
758        item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
759
760        item = menu.add(MENU_GROUP_DELETE, MENU_DELETE, 0, R.string.delete_event_label);
761        item.setIcon(android.R.drawable.ic_menu_delete);
762        item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
763
764        super.onCreateOptionsMenu(menu, inflater);
765    }
766
767    @Override
768    public void onPrepareOptionsMenu(Menu menu) {
769        boolean canAddReminders = canAddReminders();
770        menu.setGroupVisible(MENU_GROUP_REMINDER, canAddReminders);
771        menu.setGroupEnabled(MENU_GROUP_REMINDER, canAddReminders);
772
773        menu.setGroupVisible(MENU_GROUP_EDIT, mCanModifyEvent);
774        menu.setGroupEnabled(MENU_GROUP_EDIT, mCanModifyEvent);
775        menu.setGroupVisible(MENU_GROUP_DELETE, mCanModifyCalendar);
776        menu.setGroupEnabled(MENU_GROUP_DELETE, mCanModifyCalendar);
777
778        super.onPrepareOptionsMenu(menu);
779    }
780
781    @Override
782    public boolean onOptionsItemSelected(MenuItem item) {
783        super.onOptionsItemSelected(item);
784        switch (item.getItemId()) {
785            case MENU_ADD_REMINDER:
786                addReminder();
787                break;
788            case MENU_EDIT:
789                doEdit();
790                break;
791            case MENU_DELETE:
792                doDelete();
793                break;
794        }
795        return true;
796    }
797
798//CLEANUP    @Override
799//    public boolean onKeyDown(int keyCode, KeyEvent event) {
800//        if (keyCode == KeyEvent.KEYCODE_DEL) {
801//            doDelete();
802//            return true;
803//        }
804//        return super.onKeyDown(keyCode, event);
805//    }
806
807    private void updateRemindersVisibility() {
808//CLEANUP        if (mIsBusyFreeCalendar) {
809//            mRemindersContainer.setVisibility(View.GONE);
810//        } else {
811//            mRemindersContainer.setVisibility(View.VISIBLE);
812//            mReminderAdder.setVisibility(canAddReminders() ? View.VISIBLE : View.GONE);
813//        }
814    }
815
816    /**
817     * Asynchronously saves the response to an invitation if the user changed
818     * the response. Returns true if the database will be updated.
819     *
820     * @param cr the ContentResolver
821     * @return true if the database will be changed
822     */
823    private boolean saveResponse() {
824        if (mAttendeesCursor == null || mEventCursor == null) {
825            return false;
826        }
827        Spinner spinner = (Spinner) getView().findViewById(R.id.response_value);
828        int position = spinner.getSelectedItemPosition() - mResponseOffset;
829        if (position <= 0) {
830            return false;
831        }
832
833        int status = EditEventHelper.ATTENDEE_VALUES[position];
834
835        // If the status has not changed, then don't update the database
836        if (status == mOriginalAttendeeResponse) {
837            return false;
838        }
839
840        // If we never got an owner attendee id we can't set the status
841        if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE) {
842            return false;
843        }
844
845        if (!mIsRepeating) {
846            // This is a non-repeating event
847            updateResponse(mEventId, mCalendarOwnerAttendeeId, status);
848            return true;
849        }
850
851        // This is a repeating event
852        int whichEvents = mEditResponseHelper.getWhichEvents();
853        switch (whichEvents) {
854            case -1:
855                return false;
856            case UPDATE_SINGLE:
857                createExceptionResponse(mEventId, mCalendarOwnerAttendeeId, status);
858                return true;
859            case UPDATE_ALL:
860                updateResponse(mEventId, mCalendarOwnerAttendeeId, status);
861                return true;
862            default:
863                Log.e(TAG, "Unexpected choice for updating invitation response");
864                break;
865        }
866        return false;
867    }
868
869    private void updateResponse(long eventId, long attendeeId, int status) {
870        // Update the attendee status in the attendees table.  the provider
871        // takes care of updating the self attendance status.
872        ContentValues values = new ContentValues();
873
874        if (!TextUtils.isEmpty(mCalendarOwnerAccount)) {
875            values.put(Attendees.ATTENDEE_EMAIL, mCalendarOwnerAccount);
876        }
877        values.put(Attendees.ATTENDEE_STATUS, status);
878        values.put(Attendees.EVENT_ID, eventId);
879
880        Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, attendeeId);
881
882        mHandler.startUpdate(mHandler.getNextToken(), null, uri, values,
883                null, null, Utils.UNDO_DELAY);
884    }
885
886    private void createExceptionResponse(long eventId, long attendeeId,
887            int status) {
888        if (mEventCursor == null || !mEventCursor.moveToFirst()) {
889            return;
890        }
891
892        ContentValues values = new ContentValues();
893
894        String title = mEventCursor.getString(EVENT_INDEX_TITLE);
895        String timezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
896        int calendarId = mEventCursor.getInt(EVENT_INDEX_CALENDAR_ID);
897        boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
898        String syncId = mEventCursor.getString(EVENT_INDEX_SYNC_ID);
899
900        values.put(Events.TITLE, title);
901        values.put(Events.EVENT_TIMEZONE, timezone);
902        values.put(Events.ALL_DAY, allDay ? 1 : 0);
903        values.put(Events.CALENDAR_ID, calendarId);
904        values.put(Events.DTSTART, mStartMillis);
905        values.put(Events.DTEND, mEndMillis);
906        values.put(Events.ORIGINAL_EVENT, syncId);
907        values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
908        values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
909        values.put(Events.STATUS, Events.STATUS_CONFIRMED);
910        values.put(Events.SELF_ATTENDEE_STATUS, status);
911
912        // Create a recurrence exception
913        mHandler.startInsert(mHandler.getNextToken(), null,
914                Events.CONTENT_URI, values, Utils.UNDO_DELAY);
915    }
916
917    private int findResponseIndexFor(int response) {
918        int size = EditEventHelper.ATTENDEE_VALUES.length;
919        for (int index = 0; index < size; index++) {
920            if (EditEventHelper.ATTENDEE_VALUES[index] == response) {
921                return index;
922            }
923        }
924        return 0;
925    }
926
927    private void doEdit() {
928        CalendarController.getInstance(getActivity()).sendEventRelatedEvent(
929                this, EventType.EDIT_EVENT, mEventId, mStartMillis, mEndMillis, 0, 0);
930    }
931
932    private void doDelete() {
933        CalendarController.getInstance(getActivity()).sendEventRelatedEvent(
934                this, EventType.DELETE_EVENT, mEventId, mStartMillis, mEndMillis, 0, 0);
935    }
936
937    private void updateEvent(View view) {
938        if (mEventCursor == null) {
939            return;
940        }
941
942        String eventName = mEventCursor.getString(EVENT_INDEX_TITLE);
943        if (eventName == null || eventName.length() == 0) {
944            eventName = getActivity().getString(R.string.no_title_label);
945        }
946
947        boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
948        String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION);
949        String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION);
950        String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
951        boolean hasAlarm = mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0;
952        String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
953        mColor = mEventCursor.getInt(EVENT_INDEX_COLOR) & 0xbbffffff;
954
955        view.findViewById(R.id.color).setBackgroundColor(mColor);
956
957        TextView title = (TextView) view.findViewById(R.id.title);
958        title.setTextColor(mColor);
959
960//        View divider = view.findViewById(R.id.divider);
961//        divider.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
962
963        // What
964        if (eventName != null) {
965            setTextCommon(view, R.id.title, eventName);
966        }
967
968        // When
969        String when;
970        int flags;
971        if (allDay) {
972            flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY
973            | DateUtils.FORMAT_SHOW_DATE;
974        } else {
975            flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE;
976            if (DateFormat.is24HourFormat(getActivity())) {
977                flags |= DateUtils.FORMAT_24HOUR;
978            }
979        }
980        when = DateUtils.formatDateRange(getActivity(), mStartMillis, mEndMillis, flags);
981        setTextCommon(view, R.id.when, when);
982
983//CLEANUP        // Show the event timezone if it is different from the local timezone
984//        Time time = new Time();
985//        String localTimezone = time.timezone;
986//        if (allDay) {
987//            localTimezone = Time.TIMEZONE_UTC;
988//        }
989//        if (eventTimezone != null && !localTimezone.equals(eventTimezone) && !allDay) {
990//            String displayName;
991//            TimeZone tz = TimeZone.getTimeZone(localTimezone);
992//            if (tz == null || tz.getID().equals("GMT")) {
993//                displayName = localTimezone;
994//            } else {
995//                displayName = tz.getDisplayName();
996//            }
997//
998//            setTextCommon(view, R.id.timezone, displayName);
999//            setVisibilityCommon(view, R.id.timezone_container, View.VISIBLE);
1000//        } else {
1001//            setVisibilityCommon(view, R.id.timezone_container, View.GONE);
1002//        }
1003
1004        // Repeat
1005        if (rRule != null) {
1006            EventRecurrence eventRecurrence = new EventRecurrence();
1007            eventRecurrence.parse(rRule);
1008            Time date = new Time();
1009            if (allDay) {
1010                date.timezone = Time.TIMEZONE_UTC;
1011            }
1012            date.set(mStartMillis);
1013            eventRecurrence.setStartDate(date);
1014            String repeatString = EventRecurrenceFormatter.getRepeatString(
1015                    getActivity().getResources(), eventRecurrence);
1016            setTextCommon(view, R.id.repeat, repeatString);
1017            setVisibilityCommon(view, R.id.repeat_container, View.VISIBLE);
1018        } else {
1019            setVisibilityCommon(view, R.id.repeat_container, View.GONE);
1020        }
1021
1022        // Where
1023        if (location == null || location.length() == 0) {
1024            setVisibilityCommon(view, R.id.where, View.GONE);
1025        } else {
1026            final TextView textView = (TextView) view.findViewById(R.id.where);
1027            if (textView != null) {
1028                    textView.setAutoLinkMask(0);
1029                    textView.setText(location);
1030                    Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q=");
1031                    textView.setOnTouchListener(new OnTouchListener() {
1032                        public boolean onTouch(View v, MotionEvent event) {
1033                            try {
1034                                return v.onTouchEvent(event);
1035                            } catch (ActivityNotFoundException e) {
1036                                // ignore
1037                                return true;
1038                            }
1039                        }
1040                    });
1041            }
1042        }
1043
1044        // Description
1045        if (description == null || description.length() == 0) {
1046            setVisibilityCommon(view, R.id.description, View.GONE);
1047        } else {
1048            setTextCommon(view, R.id.description, description);
1049        }
1050    }
1051
1052    private void updateCalendar(View view) {
1053        mCalendarOwnerAccount = "";
1054        if (mCalendarsCursor != null && mEventCursor != null) {
1055            mCalendarsCursor.moveToFirst();
1056            String tempAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
1057            mCalendarOwnerAccount = (tempAccount == null) ? "" : tempAccount;
1058            mOrganizerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0;
1059
1060            String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
1061
1062            // start duplicate calendars query
1063            mHandler.startQuery(TOKEN_QUERY_DUPLICATE_CALENDARS, null, Calendars.CONTENT_URI,
1064                    CALENDARS_PROJECTION, CALENDARS_DUPLICATE_NAME_WHERE,
1065                    new String[] {displayName}, null);
1066
1067            String eventOrganizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER);
1068            mIsOrganizer = mCalendarOwnerAccount.equalsIgnoreCase(eventOrganizer);
1069            mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0;
1070            mOrganizer = eventOrganizer;
1071            mCanModifyCalendar =
1072                    mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) >= Calendars.CONTRIBUTOR_ACCESS;
1073            mIsBusyFreeCalendar =
1074                    mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.FREEBUSY_ACCESS;
1075            mCanModifyEvent = mCanModifyCalendar
1076                    && (mIsOrganizer || (mEventCursor.getInt(EVENT_INDEX_GUESTS_CAN_MODIFY) != 0));
1077            if (mCanModifyEvent) {
1078                Button b = (Button) mView.findViewById(R.id.edit);
1079                b.setOnClickListener(new OnClickListener() {
1080                    @Override
1081                    public void onClick(View v) {
1082                        doEdit();
1083                        EventInfoFragment.this.dismiss();
1084                    }});
1085                b.setVisibility(View.VISIBLE);
1086            }
1087        } else {
1088//CLEANUP            setVisibilityCommon(view, R.id.calendar_container, View.GONE);
1089        }
1090    }
1091
1092    private void updateAttendees(View view) {
1093        TextView tv = (TextView) view.findViewById(R.id.attendee_list);
1094        SpannableStringBuilder sb = new SpannableStringBuilder();
1095        formatAttendees(mAcceptedAttendees, sb, Attendees.ATTENDEE_STATUS_ACCEPTED);
1096        formatAttendees(mDeclinedAttendees, sb, Attendees.ATTENDEE_STATUS_DECLINED);
1097        formatAttendees(mTentativeAttendees, sb, Attendees.ATTENDEE_STATUS_TENTATIVE);
1098        formatAttendees(mNoResponseAttendees, sb, Attendees.ATTENDEE_STATUS_NONE);
1099        tv.setText(sb);
1100
1101//CLEANUP        LinearLayout attendeesLayout = (LinearLayout) view.findViewById(R.id.attendee_list);
1102//        attendeesLayout.removeAllViewsInLayout();
1103//        ++mUpdateCounts;
1104//        if(mAcceptedAttendees.size() == 0 && mDeclinedAttendees.size() == 0 &&
1105//                mTentativeAttendees.size() == mNoResponseAttendees.size()) {
1106//            // If all guests have no response just list them as guests,
1107//            CharSequence guestsLabel =
1108//                getActivity().getResources().getText(R.string.attendees_label);
1109//            addAttendeesToLayout(mNoResponseAttendees, attendeesLayout, guestsLabel);
1110//        } else {
1111//            // If we have any responses then divide them up by response
1112//            CharSequence[] entries;
1113//            entries = getActivity().getResources().getTextArray(R.array.response_labels2);
1114//            addAttendeesToLayout(mAcceptedAttendees, attendeesLayout, entries[0]);
1115//            addAttendeesToLayout(mDeclinedAttendees, attendeesLayout, entries[2]);
1116//            addAttendeesToLayout(mTentativeAttendees, attendeesLayout, entries[1]);
1117//        }
1118    }
1119
1120    private void formatAttendees(ArrayList<Attendee> attendees, SpannableStringBuilder sb, int type) {
1121        if (attendees.size() <= 0) {
1122            return;
1123        }
1124
1125        int begin = sb.length();
1126        boolean firstTime = sb.length() == 0;
1127
1128        if (firstTime == false) {
1129            begin += 2; // skip over the ", " for formatting.
1130        }
1131
1132        for (Attendee attendee : attendees) {
1133            if (firstTime) {
1134                firstTime = false;
1135            } else {
1136                sb.append(", ");
1137            }
1138
1139            String name = attendee.getDisplayName();
1140            sb.append(name);
1141        }
1142
1143        switch (type) {
1144            case Attendees.ATTENDEE_STATUS_ACCEPTED:
1145                break;
1146            case Attendees.ATTENDEE_STATUS_DECLINED:
1147                sb.setSpan(new StrikethroughSpan(), begin, sb.length(),
1148                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1149                // fall through
1150            default:
1151                // The last INCLUSIVE causes the foreground color to be applied
1152                // to the rest of the span. If not, the comma at the end of the
1153                // declined or tentative may be black.
1154                sb.setSpan(new ForegroundColorSpan(0xFF888888), begin, sb.length(),
1155                        Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
1156                break;
1157        }
1158    }
1159
1160    private void addAttendeesToLayout(ArrayList<Attendee> attendees, LinearLayout attendeeList,
1161            CharSequence sectionTitle) {
1162        if (attendees.size() == 0) {
1163            return;
1164        }
1165
1166        // Yes/No/Maybe Title
1167        View titleView = mLayoutInflater.inflate(R.layout.contact_item, null);
1168        titleView.findViewById(R.id.badge).setVisibility(View.GONE);
1169        View divider = titleView.findViewById(R.id.separator);
1170        divider.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
1171
1172        TextView title = (TextView) titleView.findViewById(R.id.name);
1173        title.setText(getActivity().getString(R.string.response_label, sectionTitle,
1174                attendees.size()));
1175        title.setTextAppearance(getActivity(), R.style.TextAppearance_EventInfo_Label);
1176        attendeeList.addView(titleView);
1177
1178        // Attendees
1179        int numOfAttendees = attendees.size();
1180        StringBuilder selection = new StringBuilder(Email.DATA + " IN (");
1181        String[] selectionArgs = new String[numOfAttendees];
1182
1183        for (int i = 0; i < numOfAttendees; ++i) {
1184            Attendee attendee = attendees.get(i);
1185            selectionArgs[i] = attendee.mEmail;
1186
1187            View v = mLayoutInflater.inflate(R.layout.contact_item, null);
1188            v.setTag(attendee);
1189
1190            View separator = v.findViewById(R.id.separator);
1191            separator.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
1192
1193            // Text
1194            TextView tv = (TextView) v.findViewById(R.id.name);
1195            String name = attendee.mName;
1196            if (name == null || name.length() == 0) {
1197                name = attendee.mEmail;
1198            }
1199            tv.setText(name);
1200
1201            ViewHolder vh = new ViewHolder();
1202            vh.badge = (QuickContactBadge) v.findViewById(R.id.badge);
1203            vh.badge.assignContactFromEmail(attendee.mEmail, true);
1204            vh.presence = (ImageView) v.findViewById(R.id.presence);
1205            mViewHolders.put(attendee.mEmail, vh);
1206
1207            if (i == 0) {
1208                selection.append('?');
1209            } else {
1210                selection.append(", ?");
1211            }
1212
1213            attendeeList.addView(v);
1214        }
1215        selection.append(')');
1216
1217        mPresenceQueryHandler.startQuery(mUpdateCounts, attendees, CONTACT_DATA_WITH_PRESENCE_URI,
1218                PRESENCE_PROJECTION, selection.toString(), selectionArgs, null);
1219    }
1220
1221    private class PresenceQueryHandler extends AsyncQueryHandler {
1222        Context mContext;
1223
1224        public PresenceQueryHandler(Context context, ContentResolver cr) {
1225            super(cr);
1226            mContext = context;
1227        }
1228
1229        @Override
1230        protected void onQueryComplete(int queryIndex, Object cookie, Cursor cursor) {
1231            if (cursor == null) {
1232                if (DEBUG) {
1233                    Log.e(TAG, "onQueryComplete: cursor == null");
1234                }
1235                return;
1236            }
1237
1238            try {
1239                cursor.moveToPosition(-1);
1240                while (cursor.moveToNext()) {
1241                    String email = cursor.getString(PRESENCE_PROJECTION_EMAIL_INDEX);
1242                    int contactId = cursor.getInt(PRESENCE_PROJECTION_CONTACT_ID_INDEX);
1243                    ViewHolder vh = mViewHolders.get(email);
1244                    int photoId = cursor.getInt(PRESENCE_PROJECTION_PHOTO_ID_INDEX);
1245                    if (DEBUG) {
1246                        Log.e(TAG, "onQueryComplete Id: " + contactId + " PhotoId: " + photoId
1247                                + " Email: " + email);
1248                    }
1249                    if (vh == null) {
1250                        continue;
1251                    }
1252                    ImageView presenceView = vh.presence;
1253                    if (presenceView != null) {
1254                        int status = cursor.getInt(PRESENCE_PROJECTION_PRESENCE_INDEX);
1255                        presenceView.setImageResource(Presence.getPresenceIconResourceId(status));
1256                        presenceView.setVisibility(View.VISIBLE);
1257                    }
1258
1259                    if (photoId > 0 && vh.updateCounts < queryIndex) {
1260                        vh.updateCounts = queryIndex;
1261                        Uri personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
1262                                contactId);
1263
1264                        // TODO, modify to batch queries together
1265                        ContactsAsyncHelper.updateImageViewWithContactPhotoAsync(mContext,
1266                                vh.badge, personUri, R.drawable.ic_contact_picture);
1267                    }
1268                }
1269            } finally {
1270                cursor.close();
1271            }
1272        }
1273    }
1274
1275    void updateResponse(View view) {
1276        // we only let the user accept/reject/etc. a meeting if:
1277        // a) you can edit the event's containing calendar AND
1278        // b) you're not the organizer and only attendee AND
1279        // c) organizerCanRespond is enabled for the calendar
1280        // (if the attendee data has been hidden, the visible number of attendees
1281        // will be 1 -- the calendar owner's).
1282        // (there are more cases involved to be 100% accurate, such as
1283        // paying attention to whether or not an attendee status was
1284        // included in the feed, but we're currently omitting those corner cases
1285        // for simplicity).
1286//CLEANUP
1287        if (!mCanModifyCalendar || (mHasAttendeeData && mIsOrganizer && mNumOfAttendees <= 1) ||
1288                (mIsOrganizer && !mOrganizerCanRespond)) {
1289            setVisibilityCommon(view, R.id.response_container, View.GONE);
1290            return;
1291        }
1292
1293        setVisibilityCommon(view, R.id.response_container, View.VISIBLE);
1294
1295        Spinner spinner = (Spinner) view.findViewById(R.id.response_value);
1296
1297        mResponseOffset = 0;
1298
1299        /* If the user has previously responded to this event
1300         * we should not allow them to select no response again.
1301         * Switch the entries to a set of entries without the
1302         * no response option.
1303         */
1304        if ((mOriginalAttendeeResponse != Attendees.ATTENDEE_STATUS_INVITED)
1305                && (mOriginalAttendeeResponse != EditEventHelper.ATTENDEE_NO_RESPONSE)
1306                && (mOriginalAttendeeResponse != Attendees.ATTENDEE_STATUS_NONE)) {
1307            CharSequence[] entries;
1308            entries = getActivity().getResources().getTextArray(R.array.response_labels2);
1309            mResponseOffset = -1;
1310            ArrayAdapter<CharSequence> adapter =
1311                new ArrayAdapter<CharSequence>(getActivity(),
1312                        android.R.layout.simple_spinner_item, entries);
1313            adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1314            spinner.setAdapter(adapter);
1315        }
1316
1317        int index;
1318        if (mAttendeeResponseFromIntent != EditEventHelper.ATTENDEE_NO_RESPONSE) {
1319            index = findResponseIndexFor(mAttendeeResponseFromIntent);
1320        } else {
1321            index = findResponseIndexFor(mOriginalAttendeeResponse);
1322        }
1323        spinner.setSelection(index + mResponseOffset);
1324        spinner.setOnItemSelectedListener(this);
1325    }
1326
1327    private void setTextCommon(View view, int id, CharSequence text) {
1328        TextView textView = (TextView) view.findViewById(id);
1329        if (textView == null)
1330            return;
1331        textView.setText(text);
1332    }
1333
1334    private void setVisibilityCommon(View view, int id, int visibility) {
1335        View v = view.findViewById(id);
1336        if (v != null) {
1337            v.setVisibility(visibility);
1338        }
1339        return;
1340    }
1341
1342    /**
1343     * Taken from com.google.android.gm.HtmlConversationActivity
1344     *
1345     * Send the intent that shows the Contact info corresponding to the email address.
1346     */
1347    public void showContactInfo(Attendee attendee, Rect rect) {
1348        // First perform lookup query to find existing contact
1349        final ContentResolver resolver = getActivity().getContentResolver();
1350        final String address = attendee.mEmail;
1351        final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI,
1352                Uri.encode(address));
1353        final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri);
1354
1355        if (lookupUri != null) {
1356            // Found matching contact, trigger QuickContact
1357            QuickContact.showQuickContact(getActivity(), rect, lookupUri,
1358                    QuickContact.MODE_MEDIUM, null);
1359        } else {
1360            // No matching contact, ask user to create one
1361            final Uri mailUri = Uri.fromParts("mailto", address, null);
1362            final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, mailUri);
1363
1364            // Pass along full E-mail string for possible create dialog
1365            Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null);
1366            intent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION, sender.toString());
1367
1368            // Only provide personal name hint if we have one
1369            final String senderPersonal = attendee.mName;
1370            if (!TextUtils.isEmpty(senderPersonal)) {
1371                intent.putExtra(Intents.Insert.NAME, senderPersonal);
1372            }
1373
1374            startActivity(intent);
1375        }
1376    }
1377}
1378