EventInfoFragment.java revision c41b99be504e395b8c7264a0d626906ccbff1c57
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        Context c = getActivity();
678        // This ensures that we aren't in the process of closing and have been
679        // unattached already
680        if (c != null) {
681            CalendarController.getInstance(c).sendEventRelatedEvent(
682                    this, EventType.EDIT_EVENT, mEventId, mStartMillis, mEndMillis, 0, 0);
683        }
684    }
685
686    private void updateEvent(View view) {
687        if (mEventCursor == null) {
688            return;
689        }
690
691        String eventName = mEventCursor.getString(EVENT_INDEX_TITLE);
692        if (eventName == null || eventName.length() == 0) {
693            eventName = getActivity().getString(R.string.no_title_label);
694        }
695
696        boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
697        String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION);
698        String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION);
699        String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
700        String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
701        mColor = mEventCursor.getInt(EVENT_INDEX_COLOR) & 0xbbffffff;
702
703        view.findViewById(R.id.color).setBackgroundColor(mColor);
704
705        TextView title = (TextView) view.findViewById(R.id.title);
706
707//        View divider = view.findViewById(R.id.divider);
708//        divider.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
709
710        // What
711        if (eventName != null) {
712            setTextCommon(view, R.id.title, eventName);
713        }
714
715        // When
716        String when;
717        int flags = DateUtils.FORMAT_SHOW_DATE;
718        if (allDay) {
719            flags |= DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY;
720        } else {
721            flags |= DateUtils.FORMAT_SHOW_TIME;
722            if (DateFormat.is24HourFormat(getActivity())) {
723                flags |= DateUtils.FORMAT_24HOUR;
724            }
725        }
726        when = Utils.formatDateRange(getActivity(), mStartMillis, mEndMillis, flags);
727        setTextCommon(view, R.id.when, when);
728
729//CLEANUP        // Show the event timezone if it is different from the local timezone
730//        Time time = new Time();
731//        String localTimezone = time.timezone;
732//        if (allDay) {
733//            localTimezone = Time.TIMEZONE_UTC;
734//        }
735//        if (eventTimezone != null && !localTimezone.equals(eventTimezone) && !allDay) {
736//            String displayName;
737//            TimeZone tz = TimeZone.getTimeZone(localTimezone);
738//            if (tz == null || tz.getID().equals("GMT")) {
739//                displayName = localTimezone;
740//            } else {
741//                displayName = tz.getDisplayName();
742//            }
743//
744//            setTextCommon(view, R.id.timezone, displayName);
745//            setVisibilityCommon(view, R.id.timezone_container, View.VISIBLE);
746//        } else {
747//            setVisibilityCommon(view, R.id.timezone_container, View.GONE);
748//        }
749
750        // Repeat
751        if (rRule != null) {
752            EventRecurrence eventRecurrence = new EventRecurrence();
753            eventRecurrence.parse(rRule);
754            Time date = new Time(Utils.getTimeZone(getActivity(), mTZUpdater));
755            if (allDay) {
756                date.timezone = Time.TIMEZONE_UTC;
757            }
758            date.set(mStartMillis);
759            eventRecurrence.setStartDate(date);
760            String repeatString = EventRecurrenceFormatter.getRepeatString(
761                    getActivity().getResources(), eventRecurrence);
762            setTextCommon(view, R.id.repeat, repeatString);
763            setVisibilityCommon(view, R.id.repeat_container, View.VISIBLE);
764        } else {
765            setVisibilityCommon(view, R.id.repeat_container, View.GONE);
766        }
767
768        // Where
769        if (location == null || location.length() == 0) {
770            setVisibilityCommon(view, R.id.where, View.GONE);
771        } else {
772            final TextView textView = (TextView) view.findViewById(R.id.where);
773            if (textView != null) {
774                    textView.setAutoLinkMask(0);
775                    textView.setText(location);
776                    Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q=");
777                    textView.setOnTouchListener(new OnTouchListener() {
778                        public boolean onTouch(View v, MotionEvent event) {
779                            try {
780                                return v.onTouchEvent(event);
781                            } catch (ActivityNotFoundException e) {
782                                // ignore
783                                return true;
784                            }
785                        }
786                    });
787            }
788        }
789
790        // Description
791        if (description != null && description.length() != 0) {
792            setTextCommon(view, R.id.description, description);
793        }
794    }
795
796    private void updateCalendar(View view) {
797        mCalendarOwnerAccount = "";
798        if (mCalendarsCursor != null && mEventCursor != null) {
799            mCalendarsCursor.moveToFirst();
800            String tempAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
801            mCalendarOwnerAccount = (tempAccount == null) ? "" : tempAccount;
802            mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0;
803
804            String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
805
806            // start duplicate calendars query
807            mHandler.startQuery(TOKEN_QUERY_DUPLICATE_CALENDARS, null, Calendars.CONTENT_URI,
808                    CALENDARS_PROJECTION, CALENDARS_DUPLICATE_NAME_WHERE,
809                    new String[] {displayName}, null);
810
811            String eventOrganizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER);
812            mIsOrganizer = mCalendarOwnerAccount.equalsIgnoreCase(eventOrganizer);
813            mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0;
814            mCanModifyCalendar =
815                    mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) >= Calendars.CONTRIBUTOR_ACCESS;
816            mIsBusyFreeCalendar =
817                    mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.FREEBUSY_ACCESS;
818
819            if (!mIsBusyFreeCalendar) {
820                Button b = (Button) mView.findViewById(R.id.edit);
821                b.setEnabled(true);
822                b.setOnClickListener(new OnClickListener() {
823                    @Override
824                    public void onClick(View v) {
825                        doEdit();
826                        EventInfoFragment.this.dismiss();
827                    }
828                });
829            }
830        } else {
831            setVisibilityCommon(view, R.id.calendar, View.GONE);
832        }
833    }
834
835    private void updateAttendees(View view) {
836        TextView tv = (TextView) view.findViewById(R.id.attendee_list);
837        SpannableStringBuilder sb = new SpannableStringBuilder();
838        formatAttendees(mAcceptedAttendees, sb, Attendees.ATTENDEE_STATUS_ACCEPTED);
839        formatAttendees(mDeclinedAttendees, sb, Attendees.ATTENDEE_STATUS_DECLINED);
840        formatAttendees(mTentativeAttendees, sb, Attendees.ATTENDEE_STATUS_TENTATIVE);
841        formatAttendees(mNoResponseAttendees, sb, Attendees.ATTENDEE_STATUS_NONE);
842
843        if (sb.length() > 0) {
844            // Add the label after the attendees are formatted because
845            // formatAttendees would prepend ", " if sb.length != 0
846            String label = getActivity().getResources().getString(R.string.attendees_label);
847            sb.insert(0, label);
848            sb.insert(label.length(), " ");
849            sb.setSpan(new StyleSpan(Typeface.BOLD), 0, label.length(),
850                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
851
852            tv.setText(sb);
853        }
854    }
855
856    private void formatAttendees(ArrayList<Attendee> attendees, SpannableStringBuilder sb, int type) {
857        if (attendees.size() <= 0) {
858            return;
859        }
860
861        int begin = sb.length();
862        boolean firstTime = sb.length() == 0;
863
864        if (firstTime == false) {
865            begin += 2; // skip over the ", " for formatting.
866        }
867
868        for (Attendee attendee : attendees) {
869            if (firstTime) {
870                firstTime = false;
871            } else {
872                sb.append(", ");
873            }
874
875            String name = attendee.getDisplayName();
876            sb.append(name);
877        }
878
879        switch (type) {
880            case Attendees.ATTENDEE_STATUS_ACCEPTED:
881                break;
882            case Attendees.ATTENDEE_STATUS_DECLINED:
883                sb.setSpan(new StrikethroughSpan(), begin, sb.length(),
884                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
885                // fall through
886            default:
887                // The last INCLUSIVE causes the foreground color to be applied
888                // to the rest of the span. If not, the comma at the end of the
889                // declined or tentative may be black.
890                sb.setSpan(new ForegroundColorSpan(0xFF888888), begin, sb.length(),
891                        Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
892                break;
893        }
894    }
895
896    void updateResponse(View view) {
897        // we only let the user accept/reject/etc. a meeting if:
898        // a) you can edit the event's containing calendar AND
899        // b) you're not the organizer and only attendee AND
900        // c) organizerCanRespond is enabled for the calendar
901        // (if the attendee data has been hidden, the visible number of attendees
902        // will be 1 -- the calendar owner's).
903        // (there are more cases involved to be 100% accurate, such as
904        // paying attention to whether or not an attendee status was
905        // included in the feed, but we're currently omitting those corner cases
906        // for simplicity).
907
908        // TODO Switch to EditEventHelper.canRespond when this class uses CalendarEventModel.
909        if (!mCanModifyCalendar || (mHasAttendeeData && mIsOrganizer && mNumOfAttendees <= 1) ||
910                (mIsOrganizer && !mOwnerCanRespond)) {
911            setVisibilityCommon(view, R.id.response_container, View.GONE);
912            return;
913        }
914
915        setVisibilityCommon(view, R.id.response_container, View.VISIBLE);
916
917
918        int response;
919        if (mAttendeeResponseFromIntent != EditEventHelper.ATTENDEE_NO_RESPONSE) {
920            response = mAttendeeResponseFromIntent;
921        } else {
922            response = mOriginalAttendeeResponse;
923        }
924
925        int buttonToCheck = findButtonIdForResponse(response);
926        RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.response_value);
927        radioGroup.check(buttonToCheck); // -1 clear all radio buttons
928        radioGroup.setOnCheckedChangeListener(this);
929    }
930
931    private void setTextCommon(View view, int id, CharSequence text) {
932        TextView textView = (TextView) view.findViewById(id);
933        if (textView == null)
934            return;
935        textView.setText(text);
936    }
937
938    private void setVisibilityCommon(View view, int id, int visibility) {
939        View v = view.findViewById(id);
940        if (v != null) {
941            v.setVisibility(visibility);
942        }
943        return;
944    }
945
946    /**
947     * Taken from com.google.android.gm.HtmlConversationActivity
948     *
949     * Send the intent that shows the Contact info corresponding to the email address.
950     */
951    public void showContactInfo(Attendee attendee, Rect rect) {
952        // First perform lookup query to find existing contact
953        final ContentResolver resolver = getActivity().getContentResolver();
954        final String address = attendee.mEmail;
955        final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI,
956                Uri.encode(address));
957        final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri);
958
959        if (lookupUri != null) {
960            // Found matching contact, trigger QuickContact
961            QuickContact.showQuickContact(getActivity(), rect, lookupUri,
962                    QuickContact.MODE_MEDIUM, null);
963        } else {
964            // No matching contact, ask user to create one
965            final Uri mailUri = Uri.fromParts("mailto", address, null);
966            final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, mailUri);
967
968            // Pass along full E-mail string for possible create dialog
969            Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null);
970            intent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION, sender.toString());
971
972            // Only provide personal name hint if we have one
973            final String senderPersonal = attendee.mName;
974            if (!TextUtils.isEmpty(senderPersonal)) {
975                intent.putExtra(Intents.Insert.NAME, senderPersonal);
976            }
977
978            startActivity(intent);
979        }
980    }
981
982    @Override
983    public void eventsChanged() {
984    }
985
986    @Override
987    public long getSupportedEventTypes() {
988        return EventType.EVENTS_CHANGED;
989    }
990
991    @Override
992    public void handleEvent(EventInfo event) {
993        if (event.eventType == EventType.EVENTS_CHANGED) {
994            // reload the data
995            mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION,
996                    null, null, null);
997        }
998
999    }
1000}
1001