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