EventInfoFragment.java revision 7c6236d5553dc9f3d004ebbed794249713a11d19
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.EventInfo;
20import com.android.calendar.CalendarController.EventType;
21import com.android.calendar.event.EditEventHelper;
22
23import android.app.Activity;
24import android.app.Dialog;
25import android.app.DialogFragment;
26import android.content.ActivityNotFoundException;
27import android.content.ContentProviderOperation;
28import android.content.ContentResolver;
29import android.content.ContentUris;
30import android.content.ContentValues;
31import android.content.Context;
32import android.content.Intent;
33import android.content.res.Resources;
34import android.database.Cursor;
35import android.graphics.Rect;
36import android.graphics.Typeface;
37import android.net.Uri;
38import android.os.Bundle;
39import android.pim.EventRecurrence;
40import android.provider.Calendar;
41import android.provider.Calendar.Attendees;
42import android.provider.Calendar.Calendars;
43import android.provider.Calendar.Events;
44import android.provider.ContactsContract;
45import android.provider.ContactsContract.CommonDataKinds;
46import android.provider.ContactsContract.Intents;
47import android.provider.ContactsContract.QuickContact;
48import android.text.Spannable;
49import android.text.SpannableStringBuilder;
50import android.text.TextUtils;
51import android.text.format.DateFormat;
52import android.text.format.DateUtils;
53import android.text.format.Time;
54import android.text.style.ForegroundColorSpan;
55import android.text.style.StrikethroughSpan;
56import android.text.style.StyleSpan;
57import android.text.util.Linkify;
58import android.text.util.Rfc822Token;
59import android.util.Log;
60import android.view.Gravity;
61import android.view.LayoutInflater;
62import android.view.MotionEvent;
63import android.view.View;
64import android.view.View.OnClickListener;
65import android.view.View.OnTouchListener;
66import android.view.ViewGroup;
67import android.view.Window;
68import android.view.WindowManager;
69import android.view.accessibility.AccessibilityEvent;
70import android.view.accessibility.AccessibilityManager;
71import android.widget.AdapterView;
72import android.widget.Button;
73import android.widget.RadioButton;
74import android.widget.RadioGroup;
75import android.widget.RadioGroup.OnCheckedChangeListener;
76import android.widget.TextView;
77import android.widget.Toast;
78
79import java.util.ArrayList;
80import java.util.List;
81import java.util.regex.Pattern;
82
83public class EventInfoFragment extends DialogFragment implements OnCheckedChangeListener,
84        CalendarController.EventHandler {
85    public static final boolean DEBUG = false;
86
87    public static final String TAG = "EventInfoFragment";
88
89    private static final String BUNDLE_KEY_EVENT_ID = "key_event_id";
90    private static final String BUNDLE_KEY_START_MILLIS = "key_start_millis";
91    private static final String BUNDLE_KEY_END_MILLIS = "key_end_millis";
92    private static final String BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog";
93
94    private static final String PERIOD_SPACE = ". ";
95
96    /**
97     * These are the corresponding indices into the array of strings
98     * "R.array.change_response_labels" in the resource file.
99     */
100    static final int UPDATE_SINGLE = 0;
101    static final int UPDATE_ALL = 1;
102
103    // Query tokens for QueryHandler
104    private static final int TOKEN_QUERY_EVENT = 1 << 0;
105    private static final int TOKEN_QUERY_CALENDARS = 1 << 1;
106    private static final int TOKEN_QUERY_ATTENDEES = 1 << 2;
107    private static final int TOKEN_QUERY_DUPLICATE_CALENDARS = 1 << 3;
108    private static final int TOKEN_QUERY_ALL = TOKEN_QUERY_DUPLICATE_CALENDARS
109            | TOKEN_QUERY_ATTENDEES | TOKEN_QUERY_CALENDARS | TOKEN_QUERY_EVENT;
110    private int mCurrentQuery = 0;
111
112    private static final String[] EVENT_PROJECTION = new String[] {
113        Events._ID,                  // 0  do not remove; used in DeleteEventHelper
114        Events.TITLE,                // 1  do not remove; used in DeleteEventHelper
115        Events.RRULE,                // 2  do not remove; used in DeleteEventHelper
116        Events.ALL_DAY,              // 3  do not remove; used in DeleteEventHelper
117        Events.CALENDAR_ID,          // 4  do not remove; used in DeleteEventHelper
118        Events.DTSTART,              // 5  do not remove; used in DeleteEventHelper
119        Events._SYNC_ID,             // 6  do not remove; used in DeleteEventHelper
120        Events.EVENT_TIMEZONE,       // 7  do not remove; used in DeleteEventHelper
121        Events.DESCRIPTION,          // 8
122        Events.EVENT_LOCATION,       // 9
123        Calendars.ACCESS_LEVEL,      // 10
124        Calendars.COLOR,             // 11
125        Events.HAS_ATTENDEE_DATA,    // 12
126        Events.ORGANIZER,            // 13
127        Events.ORIGINAL_EVENT        // 14 do not remove; used in DeleteEventHelper
128    };
129    private static final int EVENT_INDEX_ID = 0;
130    private static final int EVENT_INDEX_TITLE = 1;
131    private static final int EVENT_INDEX_RRULE = 2;
132    private static final int EVENT_INDEX_ALL_DAY = 3;
133    private static final int EVENT_INDEX_CALENDAR_ID = 4;
134    private static final int EVENT_INDEX_SYNC_ID = 6;
135    private static final int EVENT_INDEX_EVENT_TIMEZONE = 7;
136    private static final int EVENT_INDEX_DESCRIPTION = 8;
137    private static final int EVENT_INDEX_EVENT_LOCATION = 9;
138    private static final int EVENT_INDEX_ACCESS_LEVEL = 10;
139    private static final int EVENT_INDEX_COLOR = 11;
140    private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 12;
141    private static final int EVENT_INDEX_ORGANIZER = 13;
142
143    private static final String[] ATTENDEES_PROJECTION = new String[] {
144        Attendees._ID,                      // 0
145        Attendees.ATTENDEE_NAME,            // 1
146        Attendees.ATTENDEE_EMAIL,           // 2
147        Attendees.ATTENDEE_RELATIONSHIP,    // 3
148        Attendees.ATTENDEE_STATUS,          // 4
149    };
150    private static final int ATTENDEES_INDEX_ID = 0;
151    private static final int ATTENDEES_INDEX_NAME = 1;
152    private static final int ATTENDEES_INDEX_EMAIL = 2;
153    private static final int ATTENDEES_INDEX_RELATIONSHIP = 3;
154    private static final int ATTENDEES_INDEX_STATUS = 4;
155
156    private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?";
157
158    private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, "
159            + Attendees.ATTENDEE_EMAIL + " ASC";
160
161    static final String[] CALENDARS_PROJECTION = new String[] {
162        Calendars._ID,           // 0
163        Calendars.DISPLAY_NAME,  // 1
164        Calendars.OWNER_ACCOUNT, // 2
165        Calendars.ORGANIZER_CAN_RESPOND // 3
166    };
167    static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
168    static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
169    static final int CALENDARS_INDEX_OWNER_CAN_RESPOND = 3;
170
171    static final String CALENDARS_WHERE = Calendars._ID + "=?";
172    static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.DISPLAY_NAME + "=?";
173
174    private View mView;
175
176    private Uri mUri;
177    private long mEventId;
178    private Cursor mEventCursor;
179    private Cursor mAttendeesCursor;
180    private Cursor mCalendarsCursor;
181    private static float mScale = 0; // Used for supporting different screen densities
182
183    private long mStartMillis;
184    private long mEndMillis;
185
186    private boolean mHasAttendeeData;
187    private boolean mIsOrganizer;
188    private long mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE;
189    private boolean mOwnerCanRespond;
190    private String mCalendarOwnerAccount;
191    private boolean mCanModifyCalendar;
192    private boolean mIsBusyFreeCalendar;
193    private int mNumOfAttendees;
194
195    private EditResponseHelper mEditResponseHelper;
196
197    private int mOriginalAttendeeResponse;
198    private int mAttendeeResponseFromIntent = CalendarController.ATTENDEE_NO_RESPONSE;
199    private boolean mIsRepeating;
200
201    private TextView mTitle;
202    private TextView mWhen;
203    private TextView mWhere;
204    private TextView mWhat;
205    private TextView mAttendees;
206    private TextView mCalendar;
207
208    private Pattern mWildcardPattern = Pattern.compile("^.*$");
209
210    ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>();
211    ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>();
212    ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>();
213    ArrayList<Attendee> mNoResponseAttendees = new ArrayList<Attendee>();
214    private int mColor;
215
216    private QueryHandler mHandler;
217
218    private Runnable mTZUpdater = new Runnable() {
219        @Override
220        public void run() {
221            updateEvent(mView);
222        }
223    };
224
225    private static int DIALOG_WIDTH = 500;
226    private static int DIALOG_HEIGHT = 600;
227    private boolean mIsDialog = false;
228    private boolean mIsPaused = true;
229    private boolean mDismissOnResume = false;
230    private int mX = -1;
231    private int mY = -1;
232
233    private class QueryHandler extends AsyncQueryService {
234        public QueryHandler(Context context) {
235            super(context);
236        }
237
238        @Override
239        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
240            // if the activity is finishing, then close the cursor and return
241            final Activity activity = getActivity();
242            if (activity == null || activity.isFinishing()) {
243                cursor.close();
244                return;
245            }
246
247            switch (token) {
248            case TOKEN_QUERY_EVENT:
249                mEventCursor = Utils.matrixCursorFromCursor(cursor);
250                if (initEventCursor()) {
251                    // The cursor is empty. This can happen if the event was
252                    // deleted.
253                    // FRAG_TODO we should no longer rely on Activity.finish()
254                    activity.finish();
255                    return;
256                }
257                updateEvent(mView);
258
259                // start calendar query
260                Uri uri = Calendars.CONTENT_URI;
261                String[] args = new String[] {
262                        Long.toString(mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID))};
263                startQuery(TOKEN_QUERY_CALENDARS, null, uri, CALENDARS_PROJECTION,
264                        CALENDARS_WHERE, args, null);
265                break;
266            case TOKEN_QUERY_CALENDARS:
267                mCalendarsCursor = Utils.matrixCursorFromCursor(cursor);
268                updateCalendar(mView);
269                // FRAG_TODO fragments shouldn't set the title anymore
270                updateTitle();
271                // update the action bar since our option set might have changed
272                activity.invalidateOptionsMenu();
273
274                if (!mIsBusyFreeCalendar) {
275                    args = new String[] { Long.toString(mEventId) };
276
277                    // start attendees query
278                    uri = Attendees.CONTENT_URI;
279                    startQuery(TOKEN_QUERY_ATTENDEES, null, uri, ATTENDEES_PROJECTION,
280                            ATTENDEES_WHERE, args, ATTENDEES_SORT_ORDER);
281                } else {
282                    sendAccessibilityEventIfQueryDone(TOKEN_QUERY_ATTENDEES);
283                }
284                break;
285            case TOKEN_QUERY_ATTENDEES:
286                mAttendeesCursor = Utils.matrixCursorFromCursor(cursor);
287                initAttendeesCursor(mView);
288                updateResponse(mView);
289                break;
290            case TOKEN_QUERY_DUPLICATE_CALENDARS:
291                Resources res = activity.getResources();
292                SpannableStringBuilder sb = new SpannableStringBuilder();
293
294                // Label
295                String label = res.getString(R.string.view_event_calendar_label);
296                sb.append(label).append(" ");
297                sb.setSpan(new StyleSpan(Typeface.BOLD), 0, label.length(),
298                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
299
300                // Calendar display name
301                String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
302                sb.append(calendarName);
303
304                // Show email account if display name is not unique and
305                // display name != email
306                String email = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
307                if (cursor.getCount() > 1 && !calendarName.equalsIgnoreCase(email)) {
308                    sb.append(" (").append(email).append(")");
309                }
310
311                mCalendar.setText(sb);
312                break;
313            }
314            cursor.close();
315            sendAccessibilityEventIfQueryDone(token);
316        }
317
318    }
319
320    private void sendAccessibilityEventIfQueryDone(int token) {
321        mCurrentQuery |= token;
322        if (mCurrentQuery == TOKEN_QUERY_ALL) {
323            sendAccessibilityEvent();
324        }
325    }
326
327    public EventInfoFragment() {
328        mUri = null;
329    }
330
331    public EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis,
332            int attendeeResponse) {
333        if (mScale == 0) {
334            mScale = context.getResources().getDisplayMetrics().density;
335            if (mScale != 1) {
336                DIALOG_WIDTH *= mScale;
337                DIALOG_HEIGHT *= mScale;
338            }
339        }
340
341        setStyle(DialogFragment.STYLE_NO_TITLE, 0);
342        mUri = uri;
343        mStartMillis = startMillis;
344        mEndMillis = endMillis;
345        mAttendeeResponseFromIntent = attendeeResponse;
346    }
347
348    public EventInfoFragment(Context context, long eventId, long startMillis, long endMillis,
349            int attendeeResponse) {
350        this(context, ContentUris.withAppendedId(Events.CONTENT_URI, eventId), startMillis,
351                endMillis, attendeeResponse);
352        mEventId = eventId;
353    }
354
355    @Override
356    public void onActivityCreated(Bundle savedInstanceState) {
357        super.onActivityCreated(savedInstanceState);
358
359        if (savedInstanceState != null) {
360            mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false);
361        }
362
363        if (mIsDialog) {
364            applyDialogParams();
365        }
366    }
367
368    private void applyDialogParams() {
369        Dialog dialog = getDialog();
370        dialog.setCanceledOnTouchOutside(true);
371
372        Window window = dialog.getWindow();
373        window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
374
375        WindowManager.LayoutParams a = window.getAttributes();
376        a.dimAmount = .4f;
377
378        a.width = DIALOG_WIDTH;
379        a.height = DIALOG_HEIGHT;
380
381        if (mX != -1 || mY != -1) {
382            a.x = mX - a.width - 64;
383            if (a.x < 0) {
384                a.x = mX + 64;
385            }
386            a.y = mY - 64;
387            a.gravity = Gravity.LEFT | Gravity.TOP;
388        }
389
390        window.setAttributes(a);
391    }
392
393    public void setDialogParams(int x, int y) {
394        mIsDialog = true;
395        mX = x;
396        mY = y;
397    }
398
399    // Implements OnCheckedChangeListener
400    @Override
401    public void onCheckedChanged(RadioGroup group, int checkedId) {
402        // If this is not a repeating event, then don't display the dialog
403        // asking which events to change.
404        if (!mIsRepeating) {
405            return;
406        }
407
408        // If the selection is the same as the original, then don't display the
409        // dialog asking which events to change.
410        if (checkedId == findButtonIdForResponse(mOriginalAttendeeResponse)) {
411            return;
412        }
413
414        // This is a repeating event. We need to ask the user if they mean to
415        // change just this one instance or all instances.
416        mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents());
417    }
418
419    public void onNothingSelected(AdapterView<?> parent) {
420    }
421
422    @Override
423    public void onAttach(Activity activity) {
424        super.onAttach(activity);
425        mEditResponseHelper = new EditResponseHelper(activity);
426        mHandler = new QueryHandler(activity);
427    }
428
429    @Override
430    public View onCreateView(LayoutInflater inflater, ViewGroup container,
431            Bundle savedInstanceState) {
432        mView = inflater.inflate(R.layout.event_info, container, false);
433        mTitle = (TextView) mView.findViewById(R.id.title);
434        mWhen = (TextView) mView.findViewById(R.id.when);
435        mWhere = (TextView) mView.findViewById(R.id.where);
436        mWhat = (TextView) mView.findViewById(R.id.description);
437        mAttendees = (TextView) mView.findViewById(R.id.attendee_list);
438        mCalendar = (TextView) mView.findViewById(R.id.calendar);
439
440        if (mUri == null) {
441            // restore event ID from bundle
442            mEventId = savedInstanceState.getLong(BUNDLE_KEY_EVENT_ID);
443            mUri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
444            mStartMillis = savedInstanceState.getLong(BUNDLE_KEY_START_MILLIS);
445            mEndMillis = savedInstanceState.getLong(BUNDLE_KEY_END_MILLIS);
446        }
447
448        // start loading the data
449        mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION,
450                null, null, null);
451
452        Button b = (Button) mView.findViewById(R.id.delete);
453        b.setOnClickListener(new OnClickListener() {
454            @Override
455            public void onClick(View v) {
456                if (!mCanModifyCalendar) {
457                    return;
458                }
459                DeleteEventHelper deleteHelper = new DeleteEventHelper(
460                        getActivity(), getActivity(), false /* exitWhenDone */);
461                deleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable);
462            }});
463
464        return mView;
465    }
466
467    private Runnable onDeleteRunnable = new Runnable() {
468        @Override
469        public void run() {
470            if (EventInfoFragment.this.mIsPaused) {
471                mDismissOnResume = true;
472                return;
473            }
474            if (EventInfoFragment.this.isVisible()) {
475                EventInfoFragment.this.dismiss();
476            }
477        }
478    };
479
480    private void updateTitle() {
481        Resources res = getActivity().getResources();
482        if (mCanModifyCalendar && !mIsOrganizer) {
483            getActivity().setTitle(res.getString(R.string.event_info_title_invite));
484        } else {
485            getActivity().setTitle(res.getString(R.string.event_info_title));
486        }
487    }
488
489    /**
490     * Initializes the event cursor, which is expected to point to the first
491     * (and only) result from a query.
492     * @return true if the cursor is empty.
493     */
494    private boolean initEventCursor() {
495        if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) {
496            return true;
497        }
498        mEventCursor.moveToFirst();
499        mEventId = mEventCursor.getInt(EVENT_INDEX_ID);
500        String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
501        mIsRepeating = !TextUtils.isEmpty(rRule);
502        return false;
503    }
504
505    private static class Attendee {
506        String mName;
507        String mEmail;
508
509        Attendee(String name, String email) {
510            mName = name;
511            mEmail = email;
512        }
513
514        String getDisplayName() {
515            if (TextUtils.isEmpty(mName)) {
516                return mEmail;
517            } else {
518                return mName;
519            }
520        }
521    }
522
523    @SuppressWarnings("fallthrough")
524    private void initAttendeesCursor(View view) {
525        mOriginalAttendeeResponse = CalendarController.ATTENDEE_NO_RESPONSE;
526        mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE;
527        mNumOfAttendees = 0;
528        if (mAttendeesCursor != null) {
529            mNumOfAttendees = mAttendeesCursor.getCount();
530            if (mAttendeesCursor.moveToFirst()) {
531                mAcceptedAttendees.clear();
532                mDeclinedAttendees.clear();
533                mTentativeAttendees.clear();
534                mNoResponseAttendees.clear();
535
536                do {
537                    int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
538                    String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME);
539                    String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
540
541                    if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE &&
542                            mCalendarOwnerAccount.equalsIgnoreCase(email)) {
543                        mCalendarOwnerAttendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID);
544                        mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
545                    } else {
546                        // Don't show your own status in the list because:
547                        //  1) it doesn't make sense for event without other guests.
548                        //  2) there's a spinner for that for events with guests.
549                        switch(status) {
550                            case Attendees.ATTENDEE_STATUS_ACCEPTED:
551                                mAcceptedAttendees.add(new Attendee(name, email));
552                                break;
553                            case Attendees.ATTENDEE_STATUS_DECLINED:
554                                mDeclinedAttendees.add(new Attendee(name, email));
555                                break;
556                            case Attendees.ATTENDEE_STATUS_TENTATIVE:
557                                mTentativeAttendees.add(new Attendee(name, email));
558                                break;
559                            default:
560                                mNoResponseAttendees.add(new Attendee(name, email));
561                        }
562                    }
563                } while (mAttendeesCursor.moveToNext());
564                mAttendeesCursor.moveToFirst();
565
566                updateAttendees(view);
567            }
568        }
569    }
570
571    @Override
572    public void onSaveInstanceState(Bundle outState) {
573        super.onSaveInstanceState(outState);
574        outState.putLong(BUNDLE_KEY_EVENT_ID, mEventId);
575        outState.putLong(BUNDLE_KEY_START_MILLIS, mStartMillis);
576        outState.putLong(BUNDLE_KEY_END_MILLIS, mEndMillis);
577
578        outState.putBoolean(BUNDLE_KEY_IS_DIALOG, mIsDialog);
579    }
580
581
582    @Override
583    public void onDestroyView() {
584        if (saveResponse()) {
585            Toast.makeText(getActivity(), R.string.saving_event, Toast.LENGTH_SHORT).show();
586        }
587        super.onDestroyView();
588    }
589
590    @Override
591    public void onDestroy() {
592        if (mEventCursor != null) {
593            mEventCursor.close();
594        }
595        if (mCalendarsCursor != null) {
596            mCalendarsCursor.close();
597        }
598        if (mAttendeesCursor != null) {
599            mAttendeesCursor.close();
600        }
601        super.onDestroy();
602    }
603
604    /**
605     * Asynchronously saves the response to an invitation if the user changed
606     * the response. Returns true if the database will be updated.
607     *
608     * @param cr the ContentResolver
609     * @return true if the database will be changed
610     */
611    private boolean saveResponse() {
612        if (mAttendeesCursor == null || mEventCursor == null) {
613            return false;
614        }
615
616        RadioGroup radioGroup = (RadioGroup) getView().findViewById(R.id.response_value);
617        int status = getResponseFromButtonId(radioGroup.getCheckedRadioButtonId());
618        if (status == Attendees.ATTENDEE_STATUS_NONE) {
619            return false;
620        }
621
622        // If the status has not changed, then don't update the database
623        if (status == mOriginalAttendeeResponse) {
624            return false;
625        }
626
627        // If we never got an owner attendee id we can't set the status
628        if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE) {
629            return false;
630        }
631
632        if (!mIsRepeating) {
633            // This is a non-repeating event
634            updateResponse(mEventId, mCalendarOwnerAttendeeId, status);
635            return true;
636        }
637
638        // This is a repeating event
639        int whichEvents = mEditResponseHelper.getWhichEvents();
640        switch (whichEvents) {
641            case -1:
642                return false;
643            case UPDATE_SINGLE:
644                createExceptionResponse(mEventId, mCalendarOwnerAttendeeId, status);
645                return true;
646            case UPDATE_ALL:
647                updateResponse(mEventId, mCalendarOwnerAttendeeId, status);
648                return true;
649            default:
650                Log.e(TAG, "Unexpected choice for updating invitation response");
651                break;
652        }
653        return false;
654    }
655
656    private void updateResponse(long eventId, long attendeeId, int status) {
657        // Update the attendee status in the attendees table.  the provider
658        // takes care of updating the self attendance status.
659        ContentValues values = new ContentValues();
660
661        if (!TextUtils.isEmpty(mCalendarOwnerAccount)) {
662            values.put(Attendees.ATTENDEE_EMAIL, mCalendarOwnerAccount);
663        }
664        values.put(Attendees.ATTENDEE_STATUS, status);
665        values.put(Attendees.EVENT_ID, eventId);
666
667        Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, attendeeId);
668
669        mHandler.startUpdate(mHandler.getNextToken(), null, uri, values,
670                null, null, Utils.UNDO_DELAY);
671    }
672
673    private void createExceptionResponse(long eventId, long attendeeId,
674            int status) {
675        if (mEventCursor == null || !mEventCursor.moveToFirst()) {
676            return;
677        }
678        // TODO change this fragment to build a CalendarEventModel and save via
679        // EditEventHelper
680
681        ContentValues values = new ContentValues();
682
683        String title = mEventCursor.getString(EVENT_INDEX_TITLE);
684        String timezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
685        int calendarId = mEventCursor.getInt(EVENT_INDEX_CALENDAR_ID);
686        boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
687        String syncId = mEventCursor.getString(EVENT_INDEX_SYNC_ID);
688
689        values.put(Events.TITLE, title);
690        values.put(Events.EVENT_TIMEZONE, timezone);
691        values.put(Events.ALL_DAY, allDay ? 1 : 0);
692        values.put(Events.CALENDAR_ID, calendarId);
693        values.put(Events.DTSTART, mStartMillis);
694        values.put(Events.DTEND, mEndMillis);
695        values.put(Events.ORIGINAL_EVENT, syncId);
696        values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
697        values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
698        values.put(Events.STATUS, Events.STATUS_CONFIRMED);
699        values.put(Events.SELF_ATTENDEE_STATUS, status);
700
701        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
702        int eventIdIndex = ops.size();
703
704        ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values).build());
705
706        if (mHasAttendeeData) {
707            ContentProviderOperation.Builder b;
708            // Insert the new attendees
709            for (Attendee attendee : mAcceptedAttendees) {
710                addAttendee(
711                        values, ops, eventIdIndex, attendee, Attendees.ATTENDEE_STATUS_ACCEPTED);
712            }
713            for (Attendee attendee : mDeclinedAttendees) {
714                addAttendee(
715                        values, ops, eventIdIndex, attendee, Attendees.ATTENDEE_STATUS_DECLINED);
716            }
717            for (Attendee attendee : mTentativeAttendees) {
718                addAttendee(
719                        values, ops, eventIdIndex, attendee, Attendees.ATTENDEE_STATUS_TENTATIVE);
720            }
721            for (Attendee attendee : mNoResponseAttendees) {
722                addAttendee(values, ops, eventIdIndex, attendee, Attendees.ATTENDEE_STATUS_NONE);
723            }
724        }
725
726        // Create a recurrence exception
727        mHandler.startBatch(
728                mHandler.getNextToken(), null, Calendar.AUTHORITY, ops, Utils.UNDO_DELAY);
729    }
730
731    /**
732     * @param values
733     * @param ops
734     * @param eventIdIndex
735     * @param attendee
736     */
737    private void addAttendee(ContentValues values, ArrayList<ContentProviderOperation> ops,
738            int eventIdIndex, Attendee attendee, int attendeeStatus) {
739        ContentProviderOperation.Builder b;
740        values.clear();
741        values.put(Attendees.ATTENDEE_NAME, attendee.mName);
742        values.put(Attendees.ATTENDEE_EMAIL, attendee.mEmail);
743        values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
744        values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
745        values.put(Attendees.ATTENDEE_STATUS, attendeeStatus);
746
747        b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI).withValues(values);
748        b.withValueBackReference(Attendees.EVENT_ID, eventIdIndex);
749        ops.add(b.build());
750    }
751
752    public static int getResponseFromButtonId(int buttonId) {
753        int response;
754        switch (buttonId) {
755            case R.id.response_yes:
756                response = Attendees.ATTENDEE_STATUS_ACCEPTED;
757                break;
758            case R.id.response_maybe:
759                response = Attendees.ATTENDEE_STATUS_TENTATIVE;
760                break;
761            case R.id.response_no:
762                response = Attendees.ATTENDEE_STATUS_DECLINED;
763                break;
764            default:
765                response = Attendees.ATTENDEE_STATUS_NONE;
766        }
767        return response;
768    }
769
770    public static int findButtonIdForResponse(int response) {
771        int buttonId;
772        switch (response) {
773            case Attendees.ATTENDEE_STATUS_ACCEPTED:
774                buttonId = R.id.response_yes;
775                break;
776            case Attendees.ATTENDEE_STATUS_TENTATIVE:
777                buttonId = R.id.response_maybe;
778                break;
779            case Attendees.ATTENDEE_STATUS_DECLINED:
780                buttonId = R.id.response_no;
781                break;
782                default:
783                    buttonId = -1;
784        }
785        return buttonId;
786    }
787
788    private void doEdit() {
789        Context c = getActivity();
790        // This ensures that we aren't in the process of closing and have been
791        // unattached already
792        if (c != null) {
793            CalendarController.getInstance(c).sendEventRelatedEvent(
794                    this, EventType.VIEW_EVENT_DETAILS, mEventId, mStartMillis, mEndMillis, 0, 0, -1);
795        }
796    }
797
798    private void updateEvent(View view) {
799        if (mEventCursor == null) {
800            return;
801        }
802
803        String eventName = mEventCursor.getString(EVENT_INDEX_TITLE);
804        if (eventName == null || eventName.length() == 0) {
805            eventName = getActivity().getString(R.string.no_title_label);
806        }
807
808        boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
809        String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION);
810        String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION);
811        String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
812        String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
813        mColor = mEventCursor.getInt(EVENT_INDEX_COLOR) & 0xbbffffff;
814
815        view.findViewById(R.id.color).setBackgroundColor(mColor);
816
817        TextView title = mTitle;
818
819//        View divider = view.findViewById(R.id.divider);
820//        divider.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
821
822        // What
823        if (eventName != null) {
824            setTextCommon(view, R.id.title, eventName);
825        }
826
827        // When
828        String when;
829        int flags = DateUtils.FORMAT_SHOW_DATE;
830        if (allDay) {
831            flags |= DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY;
832        } else {
833            flags |= DateUtils.FORMAT_SHOW_TIME;
834            if (DateFormat.is24HourFormat(getActivity())) {
835                flags |= DateUtils.FORMAT_24HOUR;
836            }
837        }
838        when = Utils.formatDateRange(getActivity(), mStartMillis, mEndMillis, flags);
839        setTextCommon(view, R.id.when, when);
840
841//CLEANUP        // Show the event timezone if it is different from the local timezone
842//        Time time = new Time();
843//        String localTimezone = time.timezone;
844//        if (allDay) {
845//            localTimezone = Time.TIMEZONE_UTC;
846//        }
847//        if (eventTimezone != null && !localTimezone.equals(eventTimezone) && !allDay) {
848//            String displayName;
849//            TimeZone tz = TimeZone.getTimeZone(localTimezone);
850//            if (tz == null || tz.getID().equals("GMT")) {
851//                displayName = localTimezone;
852//            } else {
853//                displayName = tz.getDisplayName();
854//            }
855//
856//            setTextCommon(view, R.id.timezone, displayName);
857//            setVisibilityCommon(view, R.id.timezone_container, View.VISIBLE);
858//        } else {
859//            setVisibilityCommon(view, R.id.timezone_container, View.GONE);
860//        }
861
862        // Repeat
863        if (!TextUtils.isEmpty(rRule)) {
864            EventRecurrence eventRecurrence = new EventRecurrence();
865            eventRecurrence.parse(rRule);
866            Time date = new Time(Utils.getTimeZone(getActivity(), mTZUpdater));
867            if (allDay) {
868                date.timezone = Time.TIMEZONE_UTC;
869            }
870            date.set(mStartMillis);
871            eventRecurrence.setStartDate(date);
872            String repeatString = EventRecurrenceFormatter.getRepeatString(
873                    getActivity().getResources(), eventRecurrence);
874            setTextCommon(view, R.id.repeat, repeatString);
875            setVisibilityCommon(view, R.id.repeat_container, View.VISIBLE);
876        } else {
877            setVisibilityCommon(view, R.id.repeat_container, View.GONE);
878        }
879
880        // Where
881        if (location == null || location.trim().length() == 0) {
882            setVisibilityCommon(view, R.id.where, View.GONE);
883        } else {
884            final TextView textView = mWhere;
885            if (textView != null) {
886                textView.setAutoLinkMask(0);
887                textView.setText(location.trim());
888                if (!Linkify.addLinks(textView, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES
889                        | Linkify.MAP_ADDRESSES)) {
890                    Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q=");
891                }
892                textView.setOnTouchListener(new OnTouchListener() {
893                    public boolean onTouch(View v, MotionEvent event) {
894                        try {
895                            return v.onTouchEvent(event);
896                        } catch (ActivityNotFoundException e) {
897                            // ignore
898                            return true;
899                        }
900                    }
901                });
902            }
903        }
904
905        // Description
906        if (description != null && description.length() != 0) {
907            setTextCommon(view, R.id.description, description);
908        }
909    }
910
911    private void sendAccessibilityEvent() {
912        AccessibilityManager am = AccessibilityManager.getInstance(getActivity());
913        if (!am.isEnabled()) {
914            return;
915        }
916
917        AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
918        event.setClassName(getClass().getName());
919        event.setPackageName(getActivity().getPackageName());
920        List<CharSequence> text = event.getText();
921
922        addFieldToAccessibilityEvent(text, mTitle);
923        addFieldToAccessibilityEvent(text, mCalendar);
924        addFieldToAccessibilityEvent(text, mWhen);
925        addFieldToAccessibilityEvent(text, mWhere);
926        addFieldToAccessibilityEvent(text, mWhat);
927        addFieldToAccessibilityEvent(text, mAttendees);
928
929        RadioGroup response = (RadioGroup) getView().findViewById(R.id.response_value);
930        if (response.getVisibility() == View.VISIBLE) {
931            int id = response.getCheckedRadioButtonId();
932            if (id != View.NO_ID) {
933                text.add(((TextView) getView().findViewById(R.id.response_label)).getText());
934                text.add((((RadioButton) (response.findViewById(id))).getText() + PERIOD_SPACE));
935            }
936        }
937
938        am.sendAccessibilityEvent(event);
939    }
940
941    /**
942     * @param text
943     */
944    private void addFieldToAccessibilityEvent(List<CharSequence> text, TextView view) {
945        String str = view.toString().trim();
946        if (!TextUtils.isEmpty(str)) {
947            text.add(mTitle.getText());
948            text.add(PERIOD_SPACE);
949        }
950    }
951
952    private void updateCalendar(View view) {
953        mCalendarOwnerAccount = "";
954        if (mCalendarsCursor != null && mEventCursor != null) {
955            mCalendarsCursor.moveToFirst();
956            String tempAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
957            mCalendarOwnerAccount = (tempAccount == null) ? "" : tempAccount;
958            mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0;
959
960            String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
961
962            // start duplicate calendars query
963            mHandler.startQuery(TOKEN_QUERY_DUPLICATE_CALENDARS, null, Calendars.CONTENT_URI,
964                    CALENDARS_PROJECTION, CALENDARS_DUPLICATE_NAME_WHERE,
965                    new String[] {displayName}, null);
966
967            String eventOrganizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER);
968            mIsOrganizer = mCalendarOwnerAccount.equalsIgnoreCase(eventOrganizer);
969            mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0;
970            mCanModifyCalendar =
971                    mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) >= Calendars.CONTRIBUTOR_ACCESS;
972            mIsBusyFreeCalendar =
973                    mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.FREEBUSY_ACCESS;
974
975            if (!mIsBusyFreeCalendar) {
976                Button b = (Button) mView.findViewById(R.id.edit);
977                b.setEnabled(true);
978                b.setOnClickListener(new OnClickListener() {
979                    @Override
980                    public void onClick(View v) {
981                        doEdit();
982                        EventInfoFragment.this.dismiss();
983                    }
984                });
985            }
986            if (!mCanModifyCalendar) {
987                mView.findViewById(R.id.delete).setEnabled(false);
988            }
989        } else {
990            setVisibilityCommon(view, R.id.calendar, View.GONE);
991            sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS);
992        }
993    }
994
995    private void updateAttendees(View view) {
996        TextView tv = mAttendees;
997        SpannableStringBuilder sb = new SpannableStringBuilder();
998        formatAttendees(mAcceptedAttendees, sb, Attendees.ATTENDEE_STATUS_ACCEPTED);
999        formatAttendees(mDeclinedAttendees, sb, Attendees.ATTENDEE_STATUS_DECLINED);
1000        formatAttendees(mTentativeAttendees, sb, Attendees.ATTENDEE_STATUS_TENTATIVE);
1001        formatAttendees(mNoResponseAttendees, sb, Attendees.ATTENDEE_STATUS_NONE);
1002
1003        if (sb.length() > 0) {
1004            // Add the label after the attendees are formatted because
1005            // formatAttendees would prepend ", " if sb.length != 0
1006            String label = getActivity().getResources().getString(R.string.attendees_label);
1007            sb.insert(0, label);
1008            sb.insert(label.length(), " ");
1009            sb.setSpan(new StyleSpan(Typeface.BOLD), 0, label.length(),
1010                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1011
1012            tv.setText(sb);
1013        }
1014    }
1015
1016    private void formatAttendees(ArrayList<Attendee> attendees, SpannableStringBuilder sb, int type) {
1017        if (attendees.size() <= 0) {
1018            return;
1019        }
1020
1021        int begin = sb.length();
1022        boolean firstTime = sb.length() == 0;
1023
1024        if (firstTime == false) {
1025            begin += 2; // skip over the ", " for formatting.
1026        }
1027
1028        for (Attendee attendee : attendees) {
1029            if (firstTime) {
1030                firstTime = false;
1031            } else {
1032                sb.append(", ");
1033            }
1034
1035            String name = attendee.getDisplayName();
1036            sb.append(name);
1037        }
1038
1039        switch (type) {
1040            case Attendees.ATTENDEE_STATUS_ACCEPTED:
1041                break;
1042            case Attendees.ATTENDEE_STATUS_DECLINED:
1043                sb.setSpan(new StrikethroughSpan(), begin, sb.length(),
1044                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1045                // fall through
1046            default:
1047                // The last INCLUSIVE causes the foreground color to be applied
1048                // to the rest of the span. If not, the comma at the end of the
1049                // declined or tentative may be black.
1050                sb.setSpan(new ForegroundColorSpan(0xFF999999), begin, sb.length(),
1051                        Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
1052                break;
1053        }
1054    }
1055
1056    void updateResponse(View view) {
1057        // we only let the user accept/reject/etc. a meeting if:
1058        // a) you can edit the event's containing calendar AND
1059        // b) you're not the organizer and only attendee AND
1060        // c) organizerCanRespond is enabled for the calendar
1061        // (if the attendee data has been hidden, the visible number of attendees
1062        // will be 1 -- the calendar owner's).
1063        // (there are more cases involved to be 100% accurate, such as
1064        // paying attention to whether or not an attendee status was
1065        // included in the feed, but we're currently omitting those corner cases
1066        // for simplicity).
1067
1068        // TODO Switch to EditEventHelper.canRespond when this class uses CalendarEventModel.
1069        if (!mCanModifyCalendar || (mHasAttendeeData && mIsOrganizer && mNumOfAttendees <= 1) ||
1070                (mIsOrganizer && !mOwnerCanRespond)) {
1071            setVisibilityCommon(view, R.id.response_container, View.GONE);
1072            return;
1073        }
1074
1075        setVisibilityCommon(view, R.id.response_container, View.VISIBLE);
1076
1077
1078        int response;
1079        if (mAttendeeResponseFromIntent != CalendarController.ATTENDEE_NO_RESPONSE) {
1080            response = mAttendeeResponseFromIntent;
1081        } else {
1082            response = mOriginalAttendeeResponse;
1083        }
1084
1085        int buttonToCheck = findButtonIdForResponse(response);
1086        RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.response_value);
1087        radioGroup.check(buttonToCheck); // -1 clear all radio buttons
1088        radioGroup.setOnCheckedChangeListener(this);
1089    }
1090
1091    private void setTextCommon(View view, int id, CharSequence text) {
1092        TextView textView = (TextView) view.findViewById(id);
1093        if (textView == null)
1094            return;
1095        textView.setText(text);
1096    }
1097
1098    private void setVisibilityCommon(View view, int id, int visibility) {
1099        View v = view.findViewById(id);
1100        if (v != null) {
1101            v.setVisibility(visibility);
1102        }
1103        return;
1104    }
1105
1106    /**
1107     * Taken from com.google.android.gm.HtmlConversationActivity
1108     *
1109     * Send the intent that shows the Contact info corresponding to the email address.
1110     */
1111    public void showContactInfo(Attendee attendee, Rect rect) {
1112        // First perform lookup query to find existing contact
1113        final ContentResolver resolver = getActivity().getContentResolver();
1114        final String address = attendee.mEmail;
1115        final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI,
1116                Uri.encode(address));
1117        final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri);
1118
1119        if (lookupUri != null) {
1120            // Found matching contact, trigger QuickContact
1121            QuickContact.showQuickContact(getActivity(), rect, lookupUri,
1122                    QuickContact.MODE_MEDIUM, null);
1123        } else {
1124            // No matching contact, ask user to create one
1125            final Uri mailUri = Uri.fromParts("mailto", address, null);
1126            final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, mailUri);
1127
1128            // Pass along full E-mail string for possible create dialog
1129            Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null);
1130            intent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION, sender.toString());
1131
1132            // Only provide personal name hint if we have one
1133            final String senderPersonal = attendee.mName;
1134            if (!TextUtils.isEmpty(senderPersonal)) {
1135                intent.putExtra(Intents.Insert.NAME, senderPersonal);
1136            }
1137
1138            startActivity(intent);
1139        }
1140    }
1141
1142    @Override
1143    public void onPause() {
1144        mIsPaused = true;
1145        mHandler.removeCallbacks(onDeleteRunnable);
1146        super.onPause();
1147    }
1148
1149    @Override
1150    public void onResume() {
1151        super.onResume();
1152        mIsPaused = false;
1153        if (mDismissOnResume) {
1154            mHandler.post(onDeleteRunnable);
1155        }
1156    }
1157
1158    @Override
1159    public void eventsChanged() {
1160    }
1161
1162    @Override
1163    public long getSupportedEventTypes() {
1164        return EventType.EVENTS_CHANGED;
1165    }
1166
1167    @Override
1168    public void handleEvent(EventInfo event) {
1169        if (event.eventType == EventType.EVENTS_CHANGED) {
1170            // reload the data
1171            mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION,
1172                    null, null, null);
1173        }
1174
1175    }
1176}
1177