EventInfoFragment.java revision 1121e409c5f504e8df75982475d8cc607d5f0dfa
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.CalendarEventModel.Attendee;
22import com.android.calendar.CalendarEventModel.ReminderEntry;
23import com.android.calendar.event.AttendeesView;
24import com.android.calendar.event.EditEventHelper;
25import com.android.calendarcommon.EventRecurrence;
26import com.android.calendar.event.EventViewUtils;
27
28import android.app.Activity;
29import android.app.Dialog;
30import android.app.DialogFragment;
31import android.app.Service;
32import android.content.ActivityNotFoundException;
33import android.content.ContentProviderOperation;
34import android.content.ContentResolver;
35import android.content.ContentUris;
36import android.content.ContentValues;
37import android.content.Context;
38import android.content.Intent;
39import android.content.SharedPreferences;
40import android.content.res.Resources;
41import android.database.Cursor;
42import android.graphics.Rect;
43import android.graphics.Typeface;
44import android.net.Uri;
45import android.os.Bundle;
46import android.provider.CalendarContract;
47import android.provider.CalendarContract.Attendees;
48import android.provider.CalendarContract.Calendars;
49import android.provider.CalendarContract.Events;
50import android.provider.CalendarContract.Reminders;
51import android.provider.ContactsContract;
52import android.provider.ContactsContract.CommonDataKinds;
53import android.provider.ContactsContract.Intents;
54import android.provider.ContactsContract.QuickContact;
55import android.text.Spannable;
56import android.text.SpannableStringBuilder;
57import android.text.TextUtils;
58import android.text.format.DateFormat;
59import android.text.format.DateUtils;
60import android.text.format.Time;
61import android.text.style.ForegroundColorSpan;
62import android.text.style.StrikethroughSpan;
63import android.text.style.StyleSpan;
64import android.text.util.Linkify;
65import android.text.util.Rfc822Token;
66import android.util.Log;
67import android.view.Gravity;
68import android.view.LayoutInflater;
69import android.view.Menu;
70import android.view.MenuInflater;
71import android.view.MenuItem;
72import android.view.MotionEvent;
73import android.view.View;
74import android.view.View.OnClickListener;
75import android.view.View.OnTouchListener;
76import android.view.ViewGroup;
77import android.view.Window;
78import android.view.WindowManager;
79import android.view.accessibility.AccessibilityEvent;
80import android.view.accessibility.AccessibilityManager;
81import android.widget.AdapterView;
82import android.widget.Button;
83import android.widget.ImageButton;
84import android.widget.LinearLayout;
85import android.widget.RadioButton;
86import android.widget.RadioGroup;
87import android.widget.ScrollView;
88import android.widget.RadioGroup.OnCheckedChangeListener;
89import android.widget.TextView;
90import android.widget.Toast;
91
92import java.util.ArrayList;
93import java.util.Arrays;
94import java.util.Collections;
95import java.util.Formatter;
96import java.util.List;
97import java.util.Locale;
98import java.util.regex.Pattern;
99import java.util.TimeZone;
100
101
102public class EventInfoFragment extends DialogFragment implements OnCheckedChangeListener,
103        CalendarController.EventHandler, OnClickListener {
104    public static final boolean DEBUG = false;
105
106    public static final String TAG = "EventInfoFragment";
107
108    protected static final String BUNDLE_KEY_EVENT_ID = "key_event_id";
109    protected static final String BUNDLE_KEY_START_MILLIS = "key_start_millis";
110    protected static final String BUNDLE_KEY_END_MILLIS = "key_end_millis";
111    protected static final String BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog";
112    protected static final String BUNDLE_KEY_ATTENDEE_RESPONSE = "key_attendee_response";
113
114    private static final String PERIOD_SPACE = ". ";
115
116    /**
117     * These are the corresponding indices into the array of strings
118     * "R.array.change_response_labels" in the resource file.
119     */
120    static final int UPDATE_SINGLE = 0;
121    static final int UPDATE_ALL = 1;
122
123    // Query tokens for QueryHandler
124    private static final int TOKEN_QUERY_EVENT = 1 << 0;
125    private static final int TOKEN_QUERY_CALENDARS = 1 << 1;
126    private static final int TOKEN_QUERY_ATTENDEES = 1 << 2;
127    private static final int TOKEN_QUERY_DUPLICATE_CALENDARS = 1 << 3;
128    private static final int TOKEN_QUERY_REMINDERS = 1 << 4;
129    private static final int TOKEN_QUERY_ALL = TOKEN_QUERY_DUPLICATE_CALENDARS
130            | TOKEN_QUERY_ATTENDEES | TOKEN_QUERY_CALENDARS | TOKEN_QUERY_EVENT
131            | TOKEN_QUERY_REMINDERS;
132    private int mCurrentQuery = 0;
133
134    private static final String[] EVENT_PROJECTION = new String[] {
135        Events._ID,                  // 0  do not remove; used in DeleteEventHelper
136        Events.TITLE,                // 1  do not remove; used in DeleteEventHelper
137        Events.RRULE,                // 2  do not remove; used in DeleteEventHelper
138        Events.ALL_DAY,              // 3  do not remove; used in DeleteEventHelper
139        Events.CALENDAR_ID,          // 4  do not remove; used in DeleteEventHelper
140        Events.DTSTART,              // 5  do not remove; used in DeleteEventHelper
141        Events._SYNC_ID,             // 6  do not remove; used in DeleteEventHelper
142        Events.EVENT_TIMEZONE,       // 7  do not remove; used in DeleteEventHelper
143        Events.DESCRIPTION,          // 8
144        Events.EVENT_LOCATION,       // 9
145        Calendars.CALENDAR_ACCESS_LEVEL,      // 10
146        Calendars.CALENDAR_COLOR,             // 11
147        Events.HAS_ATTENDEE_DATA,    // 12
148        Events.ORGANIZER,            // 13
149        Events.HAS_ALARM,            // 14
150        Calendars.MAX_REMINDERS,     //15
151        Calendars.ALLOWED_REMINDERS, // 16
152        Events.ORIGINAL_SYNC_ID      // 17 do not remove; used in DeleteEventHelper
153    };
154    private static final int EVENT_INDEX_ID = 0;
155    private static final int EVENT_INDEX_TITLE = 1;
156    private static final int EVENT_INDEX_RRULE = 2;
157    private static final int EVENT_INDEX_ALL_DAY = 3;
158    private static final int EVENT_INDEX_CALENDAR_ID = 4;
159    private static final int EVENT_INDEX_SYNC_ID = 6;
160    private static final int EVENT_INDEX_EVENT_TIMEZONE = 7;
161    private static final int EVENT_INDEX_DESCRIPTION = 8;
162    private static final int EVENT_INDEX_EVENT_LOCATION = 9;
163    private static final int EVENT_INDEX_ACCESS_LEVEL = 10;
164    private static final int EVENT_INDEX_COLOR = 11;
165    private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 12;
166    private static final int EVENT_INDEX_ORGANIZER = 13;
167    private static final int EVENT_INDEX_HAS_ALARM = 14;
168    private static final int EVENT_INDEX_MAX_REMINDERS = 15;
169    private static final int EVENT_INDEX_ALLOWED_REMINDERS = 16;
170
171
172    private static final String[] ATTENDEES_PROJECTION = new String[] {
173        Attendees._ID,                      // 0
174        Attendees.ATTENDEE_NAME,            // 1
175        Attendees.ATTENDEE_EMAIL,           // 2
176        Attendees.ATTENDEE_RELATIONSHIP,    // 3
177        Attendees.ATTENDEE_STATUS,          // 4
178    };
179    private static final int ATTENDEES_INDEX_ID = 0;
180    private static final int ATTENDEES_INDEX_NAME = 1;
181    private static final int ATTENDEES_INDEX_EMAIL = 2;
182    private static final int ATTENDEES_INDEX_RELATIONSHIP = 3;
183    private static final int ATTENDEES_INDEX_STATUS = 4;
184
185    private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?";
186
187    private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, "
188            + Attendees.ATTENDEE_EMAIL + " ASC";
189
190    private static final String[] REMINDERS_PROJECTION = new String[] {
191        Reminders._ID,                      // 0
192        Reminders.MINUTES,            // 1
193        Reminders.METHOD           // 2
194    };
195    private static final int REMINDERS_INDEX_ID = 0;
196    private static final int REMINDERS_MINUTES_ID = 1;
197    private static final int REMINDERS_METHOD_ID = 2;
198
199    private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=?";
200
201    static final String[] CALENDARS_PROJECTION = new String[] {
202        Calendars._ID,           // 0
203        Calendars.CALENDAR_DISPLAY_NAME,  // 1
204        Calendars.OWNER_ACCOUNT, // 2
205        Calendars.CAN_ORGANIZER_RESPOND // 3
206    };
207    static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
208    static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
209    static final int CALENDARS_INDEX_OWNER_CAN_RESPOND = 3;
210
211    static final String CALENDARS_WHERE = Calendars._ID + "=?";
212    static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.CALENDAR_DISPLAY_NAME + "=?";
213
214    private View mView;
215
216    private Uri mUri;
217    private long mEventId;
218    private Cursor mEventCursor;
219    private Cursor mAttendeesCursor;
220    private Cursor mCalendarsCursor;
221    private Cursor mRemindersCursor;
222
223    private static float mScale = 0; // Used for supporting different screen densities
224
225    private long mStartMillis;
226    private long mEndMillis;
227
228    private boolean mHasAttendeeData;
229    private boolean mIsOrganizer;
230    private long mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE;
231    private boolean mOwnerCanRespond;
232    private String mCalendarOwnerAccount;
233    private boolean mCanModifyCalendar;
234    private boolean mIsBusyFreeCalendar;
235    private int mNumOfAttendees;
236
237    private EditResponseHelper mEditResponseHelper;
238
239    private int mOriginalAttendeeResponse;
240    private int mAttendeeResponseFromIntent = CalendarController.ATTENDEE_NO_RESPONSE;
241    private boolean mIsRepeating;
242    private boolean mHasAlarm;
243    private int mMaxReminders;
244    private String mCalendarAllowedReminders;
245
246    private TextView mTitle;
247    private TextView mWhen;
248    private TextView mWhere;
249    private TextView mWhat;
250    private TextView mAttendees;
251    private AttendeesView mLongAttendees;
252    private Menu mMenu;
253    private View mHeadlines;
254    private ScrollView mScrollView;
255
256    private Pattern mWildcardPattern = Pattern.compile("^.*$");
257
258    ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>();
259    ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>();
260    ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>();
261    ArrayList<Attendee> mNoResponseAttendees = new ArrayList<Attendee>();
262    private int mColor;
263
264
265    private int mDefaultReminderMinutes;
266    private ArrayList<LinearLayout> mReminderViews = new ArrayList<LinearLayout>(0);
267    public ArrayList<ReminderEntry> mReminders;
268    public ArrayList<ReminderEntry> mOriginalReminders;
269
270    /**
271     * Contents of the "minutes" spinner.  This has default values from the XML file, augmented
272     * with any additional values that were already associated with the event.
273     */
274    private ArrayList<Integer> mReminderMinuteValues;
275    private ArrayList<String> mReminderMinuteLabels;
276
277    /**
278     * Contents of the "methods" spinner.  The "values" list specifies the method constant
279     * (e.g. {@link Reminders#METHOD_ALERT}) associated with the labels.  Any methods that
280     * aren't allowed by the Calendar will be removed.
281     */
282    private ArrayList<Integer> mReminderMethodValues;
283    private ArrayList<String> mReminderMethodLabels;
284
285
286
287    private QueryHandler mHandler;
288
289    private Runnable mTZUpdater = new Runnable() {
290        @Override
291        public void run() {
292            updateEvent(mView);
293        }
294    };
295
296    private static int DIALOG_WIDTH = 500;
297    private static int DIALOG_HEIGHT = 600;
298    private boolean mIsDialog = false;
299    private boolean mIsPaused = true;
300    private boolean mDismissOnResume = false;
301    private int mX = -1;
302    private int mY = -1;
303    private Button mDescButton;  // Button to expand/collapse the description
304    private String mMoreLabel;   // Labels for the button
305    private String mLessLabel;
306    private boolean mShowMaxDescription;  // Current status of button
307    private int mDescLineNum;             // The default number of lines in the description
308    private boolean mIsTabletConfig;
309    private Activity mActivity;
310
311    private class QueryHandler extends AsyncQueryService {
312        public QueryHandler(Context context) {
313            super(context);
314        }
315
316        @Override
317        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
318            // if the activity is finishing, then close the cursor and return
319            final Activity activity = getActivity();
320            if (activity == null || activity.isFinishing()) {
321                cursor.close();
322                return;
323            }
324
325            switch (token) {
326            case TOKEN_QUERY_EVENT:
327                mEventCursor = Utils.matrixCursorFromCursor(cursor);
328                if (initEventCursor()) {
329                    // The cursor is empty. This can happen if the event was
330                    // deleted.
331                    // FRAG_TODO we should no longer rely on Activity.finish()
332                    activity.finish();
333                    return;
334                }
335                updateEvent(mView);
336
337                // start calendar query
338                Uri uri = Calendars.CONTENT_URI;
339                String[] args = new String[] {
340                        Long.toString(mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID))};
341                startQuery(TOKEN_QUERY_CALENDARS, null, uri, CALENDARS_PROJECTION,
342                        CALENDARS_WHERE, args, null);
343                break;
344            case TOKEN_QUERY_CALENDARS:
345                mCalendarsCursor = Utils.matrixCursorFromCursor(cursor);
346                updateCalendar(mView);
347                // FRAG_TODO fragments shouldn't set the title anymore
348                updateTitle();
349                // update the action bar since our option set might have changed
350                activity.invalidateOptionsMenu();
351
352                if (!mIsBusyFreeCalendar) {
353                    args = new String[] { Long.toString(mEventId) };
354
355                    // start attendees query
356                    uri = Attendees.CONTENT_URI;
357                    startQuery(TOKEN_QUERY_ATTENDEES, null, uri, ATTENDEES_PROJECTION,
358                            ATTENDEES_WHERE, args, ATTENDEES_SORT_ORDER);
359                } else {
360                    sendAccessibilityEventIfQueryDone(TOKEN_QUERY_ATTENDEES);
361                }
362                mOriginalReminders = new ArrayList<ReminderEntry> ();
363                if (mHasAlarm) {
364                    // start reminders query
365                    args = new String[] { Long.toString(mEventId) };
366                    uri = Reminders.CONTENT_URI;
367                    startQuery(TOKEN_QUERY_REMINDERS, null, uri,
368                            REMINDERS_PROJECTION, REMINDERS_WHERE, args, null);
369                } else {
370                    sendAccessibilityEventIfQueryDone(TOKEN_QUERY_REMINDERS);
371                }
372                break;
373            case TOKEN_QUERY_ATTENDEES:
374                mAttendeesCursor = Utils.matrixCursorFromCursor(cursor);
375                initAttendeesCursor(mView);
376                updateResponse(mView);
377                break;
378            case TOKEN_QUERY_REMINDERS:
379                mRemindersCursor = Utils.matrixCursorFromCursor(cursor);
380                initReminders(mView, mRemindersCursor);
381                break;
382            case TOKEN_QUERY_DUPLICATE_CALENDARS:
383                Resources res = activity.getResources();
384                SpannableStringBuilder sb = new SpannableStringBuilder();
385
386                // Label
387                String label = res.getString(R.string.view_event_calendar_label);
388                sb.append(label).append(" ");
389                sb.setSpan(new StyleSpan(Typeface.BOLD), 0, label.length(),
390                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
391
392                // Calendar display name
393                String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
394                sb.append(calendarName);
395
396                // Show email account if display name is not unique and
397                // display name != email
398                String email = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
399                if (cursor.getCount() > 1 && !calendarName.equalsIgnoreCase(email)) {
400                    sb.append(" (").append(email).append(")");
401                }
402
403                break;
404            }
405            cursor.close();
406            sendAccessibilityEventIfQueryDone(token);
407        }
408
409    }
410
411    private void sendAccessibilityEventIfQueryDone(int token) {
412        mCurrentQuery |= token;
413        if (mCurrentQuery == TOKEN_QUERY_ALL) {
414            sendAccessibilityEvent();
415        }
416    }
417
418    public EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis,
419            int attendeeResponse, boolean isDialog) {
420
421        if (mScale == 0) {
422            mScale = context.getResources().getDisplayMetrics().density;
423            if (mScale != 1) {
424                DIALOG_WIDTH *= mScale;
425                DIALOG_HEIGHT *= mScale;
426            }
427        }
428        mIsDialog = isDialog;
429
430        setStyle(DialogFragment.STYLE_NO_TITLE, 0);
431        mUri = uri;
432        mStartMillis = startMillis;
433        mEndMillis = endMillis;
434        mAttendeeResponseFromIntent = attendeeResponse;
435    }
436
437    // This is currently required by the fragment manager.
438    public EventInfoFragment() {
439    }
440
441
442
443    public EventInfoFragment(Context context, long eventId, long startMillis, long endMillis,
444            int attendeeResponse, boolean isDialog) {
445        this(context, ContentUris.withAppendedId(Events.CONTENT_URI, eventId), startMillis,
446                endMillis, attendeeResponse, isDialog);
447        mEventId = eventId;
448    }
449
450    @Override
451    public void onActivityCreated(Bundle savedInstanceState) {
452        super.onActivityCreated(savedInstanceState);
453
454        if (savedInstanceState != null) {
455            mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false);
456        }
457
458        if (mIsDialog) {
459            applyDialogParams();
460        }
461    }
462
463    private void applyDialogParams() {
464        Dialog dialog = getDialog();
465        dialog.setCanceledOnTouchOutside(true);
466
467        Window window = dialog.getWindow();
468        window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
469
470        WindowManager.LayoutParams a = window.getAttributes();
471        a.dimAmount = .4f;
472
473        a.width = DIALOG_WIDTH;
474        a.height = DIALOG_HEIGHT;
475
476
477        // On tablets , do smart positioning of dialog
478        // On phones , use the whole screen
479
480        if (mX != -1 || mY != -1) {
481            a.x = mX - a.width - 64;
482            if (a.x < 0) {
483                a.x = mX + 64;
484            }
485            a.y = mY - 64;
486            a.gravity = Gravity.LEFT | Gravity.TOP;
487        }
488        window.setAttributes(a);
489    }
490
491    public void setDialogParams(int x, int y) {
492        mX = x;
493        mY = y;
494    }
495
496    // Implements OnCheckedChangeListener
497    @Override
498    public void onCheckedChanged(RadioGroup group, int checkedId) {
499        // If this is not a repeating event, then don't display the dialog
500        // asking which events to change.
501        if (!mIsRepeating) {
502            return;
503        }
504
505        // If the selection is the same as the original, then don't display the
506        // dialog asking which events to change.
507        if (checkedId == findButtonIdForResponse(mOriginalAttendeeResponse)) {
508            return;
509        }
510
511        // This is a repeating event. We need to ask the user if they mean to
512        // change just this one instance or all instances.
513        mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents());
514    }
515
516    public void onNothingSelected(AdapterView<?> parent) {
517    }
518
519    @Override
520    public void onAttach(Activity activity) {
521        super.onAttach(activity);
522        mActivity = activity;
523        mEditResponseHelper = new EditResponseHelper(activity);
524        mHandler = new QueryHandler(activity);
525        mDescLineNum = activity.getResources().getInteger((R.integer.event_info_desc_line_num));
526        mMoreLabel = activity.getResources().getString((R.string.event_info_desc_more));
527        mLessLabel = activity.getResources().getString((R.string.event_info_desc_less));
528        if (!mIsDialog) {
529            setHasOptionsMenu(true);
530        }
531    }
532
533    @Override
534    public View onCreateView(LayoutInflater inflater, ViewGroup container,
535            Bundle savedInstanceState) {
536        mView = inflater.inflate(R.layout.event_info, container, false);
537        mScrollView = (ScrollView) mView.findViewById(R.id.event_info_scroll_view);
538        mTitle = (TextView) mView.findViewById(R.id.title);
539        mWhen = (TextView) mView.findViewById(R.id.when);
540        mWhere = (TextView) mView.findViewById(R.id.where);
541        mWhat = (TextView) mView.findViewById(R.id.description);
542        mAttendees = (TextView) mView.findViewById(R.id.attendee_list);
543        mHeadlines = mView.findViewById(R.id.event_info_headline);
544        mLongAttendees = (AttendeesView)mView.findViewById(R.id.long_attendee_list);
545        mDescButton = (Button)mView.findViewById(R.id.desc_expand);
546        mDescButton.setOnClickListener(new View.OnClickListener() {
547            @Override
548            public void onClick(View v) {
549                mShowMaxDescription = !mShowMaxDescription;
550                updateDescription();
551            }
552        });
553        mShowMaxDescription = false; // Show short version of description as default.
554        mIsTabletConfig = Utils.getConfigBool(mActivity, R.bool.tablet_config);
555
556        if (mUri == null) {
557            // restore event ID from bundle
558            mEventId = savedInstanceState.getLong(BUNDLE_KEY_EVENT_ID);
559            mUri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
560            mStartMillis = savedInstanceState.getLong(BUNDLE_KEY_START_MILLIS);
561            mEndMillis = savedInstanceState.getLong(BUNDLE_KEY_END_MILLIS);
562        }
563
564        // start loading the data
565        mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION,
566                null, null, null);
567
568        Button b = (Button) mView.findViewById(R.id.delete);
569        b.setOnClickListener(new OnClickListener() {
570            @Override
571            public void onClick(View v) {
572                if (!mCanModifyCalendar) {
573                    return;
574                }
575                DeleteEventHelper deleteHelper = new DeleteEventHelper(
576                        getActivity(), getActivity(),
577                        !mIsDialog && !mIsTabletConfig /* exitWhenDone */);
578                deleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable);
579            }});
580
581        // Hide Edit/Delete buttons if in full screen mode on a phone
582        if (savedInstanceState != null) {
583            mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false);
584        }
585        if (!mIsDialog && !mIsTabletConfig) {
586            mView.findViewById(R.id.event_info_buttons_container).setVisibility(View.GONE);
587        }
588
589        // Create a listener for the add reminder button
590
591        ImageButton reminderAddButton = (ImageButton) mView.findViewById(R.id.reminder_add);
592        View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
593            @Override
594            public void onClick(View v) {
595                addReminder();
596            }
597        };
598        reminderAddButton.setOnClickListener(addReminderOnClickListener);
599
600        // Set reminders variables
601
602        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mActivity);
603        String defaultReminderString = prefs.getString(
604                GeneralPreferences.KEY_DEFAULT_REMINDER, GeneralPreferences.NO_REMINDER_STRING);
605        mDefaultReminderMinutes = Integer.parseInt(defaultReminderString);
606        prepareReminders();
607
608        return mView;
609    }
610
611    private Runnable onDeleteRunnable = new Runnable() {
612        @Override
613        public void run() {
614            if (EventInfoFragment.this.mIsPaused) {
615                mDismissOnResume = true;
616                return;
617            }
618            if (EventInfoFragment.this.isVisible()) {
619                EventInfoFragment.this.dismiss();
620            }
621        }
622    };
623
624    // Sets the description:
625    // Set the expand/collapse button
626    // Expand/collapse the description according the the current status
627    private void updateDescription() {
628        // Description is short, hide button
629        if (mWhat.getLineCount() <= mDescLineNum) {
630            mDescButton.setVisibility(View.GONE);
631            return;
632        }
633        // Show button and set label according to the expand/collapse status
634        mDescButton.setVisibility(View.VISIBLE);
635        if (mShowMaxDescription) {
636            mDescButton.setText(mLessLabel);
637            mWhat.setLines(mWhat.getLineCount());
638        } else {
639            mDescButton.setText(mMoreLabel);
640            mWhat.setLines(mDescLineNum);
641        }
642    }
643
644    private void updateTitle() {
645        Resources res = getActivity().getResources();
646        if (mCanModifyCalendar && !mIsOrganizer) {
647            getActivity().setTitle(res.getString(R.string.event_info_title_invite));
648        } else {
649            getActivity().setTitle(res.getString(R.string.event_info_title));
650        }
651    }
652
653    /**
654     * Initializes the event cursor, which is expected to point to the first
655     * (and only) result from a query.
656     * @return true if the cursor is empty.
657     */
658    private boolean initEventCursor() {
659        if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) {
660            return true;
661        }
662        mEventCursor.moveToFirst();
663        mEventId = mEventCursor.getInt(EVENT_INDEX_ID);
664        String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
665        mIsRepeating = !TextUtils.isEmpty(rRule);
666        mHasAlarm = (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) == 1)?true:false;
667        mMaxReminders = mEventCursor.getInt(EVENT_INDEX_MAX_REMINDERS);
668        mCalendarAllowedReminders =  mEventCursor.getString(EVENT_INDEX_ALLOWED_REMINDERS);
669        return false;
670    }
671
672    @SuppressWarnings("fallthrough")
673    private void initAttendeesCursor(View view) {
674        mOriginalAttendeeResponse = CalendarController.ATTENDEE_NO_RESPONSE;
675        mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE;
676        mNumOfAttendees = 0;
677        if (mAttendeesCursor != null) {
678            mNumOfAttendees = mAttendeesCursor.getCount();
679            if (mAttendeesCursor.moveToFirst()) {
680                mAcceptedAttendees.clear();
681                mDeclinedAttendees.clear();
682                mTentativeAttendees.clear();
683                mNoResponseAttendees.clear();
684
685                do {
686                    int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
687                    String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME);
688                    String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
689
690                    if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE &&
691                            mCalendarOwnerAccount.equalsIgnoreCase(email)) {
692                        mCalendarOwnerAttendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID);
693                        mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
694                    } else {
695                        // Don't show your own status in the list because:
696                        //  1) it doesn't make sense for event without other guests.
697                        //  2) there's a spinner for that for events with guests.
698                        switch(status) {
699                            case Attendees.ATTENDEE_STATUS_ACCEPTED:
700                                mAcceptedAttendees.add(new Attendee(name, email,
701                                        Attendees.ATTENDEE_STATUS_ACCEPTED));
702                                break;
703                            case Attendees.ATTENDEE_STATUS_DECLINED:
704                                mDeclinedAttendees.add(new Attendee(name, email,
705                                        Attendees.ATTENDEE_STATUS_DECLINED));
706                                break;
707                            case Attendees.ATTENDEE_STATUS_TENTATIVE:
708                                mTentativeAttendees.add(new Attendee(name, email,
709                                        Attendees.ATTENDEE_STATUS_TENTATIVE));
710                                break;
711                            default:
712                                mNoResponseAttendees.add(new Attendee(name, email,
713                                        Attendees.ATTENDEE_STATUS_NONE));
714                        }
715                    }
716                } while (mAttendeesCursor.moveToNext());
717                mAttendeesCursor.moveToFirst();
718
719                updateAttendees(view);
720            }
721        }
722    }
723
724    @Override
725    public void onSaveInstanceState(Bundle outState) {
726        super.onSaveInstanceState(outState);
727        outState.putLong(BUNDLE_KEY_EVENT_ID, mEventId);
728        outState.putLong(BUNDLE_KEY_START_MILLIS, mStartMillis);
729        outState.putLong(BUNDLE_KEY_END_MILLIS, mEndMillis);
730        outState.putBoolean(BUNDLE_KEY_IS_DIALOG, mIsDialog);
731        outState.putInt(BUNDLE_KEY_ATTENDEE_RESPONSE, mAttendeeResponseFromIntent);
732    }
733
734
735    @Override
736    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
737        super.onCreateOptionsMenu(menu, inflater);
738        // Show edit/delete buttons only in non-dialog configuration on a phone
739        if (!mIsDialog && !mIsTabletConfig) {
740            inflater.inflate(R.menu.event_info_title_bar, menu);
741                mMenu = menu;
742        }
743    }
744
745
746    @Override
747    public void onDestroyView() {
748        if (saveResponse() || saveReminders()) {
749            Toast.makeText(getActivity(), R.string.saving_event, Toast.LENGTH_SHORT).show();
750        }
751        super.onDestroyView();
752    }
753
754    @Override
755    public void onDestroy() {
756        if (mEventCursor != null) {
757            mEventCursor.close();
758        }
759        if (mCalendarsCursor != null) {
760            mCalendarsCursor.close();
761        }
762        if (mAttendeesCursor != null) {
763            mAttendeesCursor.close();
764        }
765        super.onDestroy();
766    }
767
768    /**
769     * Asynchronously saves the response to an invitation if the user changed
770     * the response. Returns true if the database will be updated.
771     *
772     * @return true if the database will be changed
773     */
774    private boolean saveResponse() {
775        if (mAttendeesCursor == null || mEventCursor == null) {
776            return false;
777        }
778
779        RadioGroup radioGroup = (RadioGroup) getView().findViewById(R.id.response_value);
780        int status = getResponseFromButtonId(radioGroup.getCheckedRadioButtonId());
781        if (status == Attendees.ATTENDEE_STATUS_NONE) {
782            return false;
783        }
784
785        // If the status has not changed, then don't update the database
786        if (status == mOriginalAttendeeResponse) {
787            return false;
788        }
789
790        // If we never got an owner attendee id we can't set the status
791        if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE) {
792            return false;
793        }
794
795        if (!mIsRepeating) {
796            // This is a non-repeating event
797            updateResponse(mEventId, mCalendarOwnerAttendeeId, status);
798            return true;
799        }
800
801        // This is a repeating event
802        int whichEvents = mEditResponseHelper.getWhichEvents();
803        switch (whichEvents) {
804            case -1:
805                return false;
806            case UPDATE_SINGLE:
807                createExceptionResponse(mEventId, status);
808                return true;
809            case UPDATE_ALL:
810                updateResponse(mEventId, mCalendarOwnerAttendeeId, status);
811                return true;
812            default:
813                Log.e(TAG, "Unexpected choice for updating invitation response");
814                break;
815        }
816        return false;
817    }
818
819    private void updateResponse(long eventId, long attendeeId, int status) {
820        // Update the attendee status in the attendees table.  the provider
821        // takes care of updating the self attendance status.
822        ContentValues values = new ContentValues();
823
824        if (!TextUtils.isEmpty(mCalendarOwnerAccount)) {
825            values.put(Attendees.ATTENDEE_EMAIL, mCalendarOwnerAccount);
826        }
827        values.put(Attendees.ATTENDEE_STATUS, status);
828        values.put(Attendees.EVENT_ID, eventId);
829
830        Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, attendeeId);
831
832        mHandler.startUpdate(mHandler.getNextToken(), null, uri, values,
833                null, null, Utils.UNDO_DELAY);
834    }
835
836    /**
837     * Creates an exception to a recurring event.  The only change we're making is to the
838     * "self attendee status" value.  The provider will take care of updating the corresponding
839     * Attendees.attendeeStatus entry.
840     *
841     * @param eventId The recurring event.
842     * @param status The new value for selfAttendeeStatus.
843     */
844    private void createExceptionResponse(long eventId, int status) {
845        ContentValues values = new ContentValues();
846        values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
847        values.put(Events.SELF_ATTENDEE_STATUS, status);
848        values.put(Events.STATUS, Events.STATUS_CONFIRMED);
849
850        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
851        Uri exceptionUri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI,
852                String.valueOf(eventId));
853        ops.add(ContentProviderOperation.newInsert(exceptionUri).withValues(values).build());
854
855        mHandler.startBatch(mHandler.getNextToken(), null, CalendarContract.AUTHORITY, ops,
856                Utils.UNDO_DELAY);
857   }
858
859    public static int getResponseFromButtonId(int buttonId) {
860        int response;
861        switch (buttonId) {
862            case R.id.response_yes:
863                response = Attendees.ATTENDEE_STATUS_ACCEPTED;
864                break;
865            case R.id.response_maybe:
866                response = Attendees.ATTENDEE_STATUS_TENTATIVE;
867                break;
868            case R.id.response_no:
869                response = Attendees.ATTENDEE_STATUS_DECLINED;
870                break;
871            default:
872                response = Attendees.ATTENDEE_STATUS_NONE;
873        }
874        return response;
875    }
876
877    public static int findButtonIdForResponse(int response) {
878        int buttonId;
879        switch (response) {
880            case Attendees.ATTENDEE_STATUS_ACCEPTED:
881                buttonId = R.id.response_yes;
882                break;
883            case Attendees.ATTENDEE_STATUS_TENTATIVE:
884                buttonId = R.id.response_maybe;
885                break;
886            case Attendees.ATTENDEE_STATUS_DECLINED:
887                buttonId = R.id.response_no;
888                break;
889                default:
890                    buttonId = -1;
891        }
892        return buttonId;
893    }
894
895    private void doEdit() {
896        Context c = getActivity();
897        // This ensures that we aren't in the process of closing and have been
898        // unattached already
899        if (c != null) {
900            CalendarController.getInstance(c).sendEventRelatedEvent(
901                    this, EventType.VIEW_EVENT_DETAILS, mEventId, mStartMillis, mEndMillis, 0
902                    , 0, -1);
903        }
904    }
905
906    private void updateEvent(View view) {
907        if (mEventCursor == null || view == null) {
908            return;
909        }
910
911        String eventName = mEventCursor.getString(EVENT_INDEX_TITLE);
912        if (eventName == null || eventName.length() == 0) {
913            eventName = getActivity().getString(R.string.no_title_label);
914        }
915
916        boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
917        String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION);
918        String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION);
919        String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
920        String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
921        String organizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER);
922
923        mColor = Utils.getDisplayColorFromColor(mEventCursor.getInt(EVENT_INDEX_COLOR));
924        mHeadlines.setBackgroundColor(mColor);
925
926        // What
927        if (eventName != null) {
928            setTextCommon(view, R.id.title, eventName);
929        }
930
931        // When
932        // Set the date and repeats (if any)
933        String whenDate;
934        int flagsTime = DateUtils.FORMAT_SHOW_TIME;
935        int flagsDate = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY |
936                DateUtils.FORMAT_SHOW_YEAR;
937
938        if (DateFormat.is24HourFormat(getActivity())) {
939            flagsTime |= DateUtils.FORMAT_24HOUR;
940        }
941
942        // Put repeat after the date (if any)
943        String repeatString = null;
944        if (!TextUtils.isEmpty(rRule)) {
945            EventRecurrence eventRecurrence = new EventRecurrence();
946            eventRecurrence.parse(rRule);
947            Time date = new Time(Utils.getTimeZone(getActivity(), mTZUpdater));
948            if (allDay) {
949                date.timezone = Time.TIMEZONE_UTC;
950            }
951            date.set(mStartMillis);
952            eventRecurrence.setStartDate(date);
953            repeatString = EventRecurrenceFormatter.getRepeatString(
954                    getActivity().getResources(), eventRecurrence);
955        }
956        // If an all day event , show the date without the time
957        if (allDay) {
958            Formatter f = new Formatter(new StringBuilder(50), Locale.getDefault());
959            whenDate = DateUtils.formatDateRange(getActivity(), f, mStartMillis, mStartMillis,
960                    flagsDate, Time.TIMEZONE_UTC).toString();
961            if (repeatString != null) {
962                setTextCommon(view, R.id.when_date, whenDate + " (" + repeatString + ")");
963            } else {
964                setTextCommon(view, R.id.when_date, whenDate);
965            }
966            view.findViewById(R.id.when_time).setVisibility(View.GONE);
967
968        } else {
969            // Show date for none all-day events
970            whenDate = Utils.formatDateRange(getActivity(), mStartMillis, mEndMillis, flagsDate);
971            String whenTime = Utils.formatDateRange(getActivity(), mStartMillis, mEndMillis,
972                    flagsTime);
973            if (repeatString != null) {
974                setTextCommon(view, R.id.when_date, whenDate + " (" + repeatString + ")");
975            } else {
976                setTextCommon(view, R.id.when_date, whenDate);
977            }
978
979            // Show the event timezone if it is different from the local timezone after the time
980            // TODO: Fix comparison of Timezone
981            String localTimezone = Utils.getTimeZone(mActivity, mTZUpdater);
982            if (!TextUtils.equals(localTimezone, eventTimezone)) {
983                String displayName;
984                // Figure out if this is in DST
985                Time date = new Time(Utils.getTimeZone(getActivity(), mTZUpdater));
986                if (allDay) {
987                    date.timezone = Time.TIMEZONE_UTC;
988                }
989                date.set(mStartMillis);
990
991                TimeZone tz = TimeZone.getTimeZone(localTimezone);
992                if (tz == null || tz.getID().equals("GMT")) {
993                    displayName = localTimezone;
994                } else {
995                    displayName = tz.getDisplayName(date.isDst != 0, TimeZone.LONG);
996                }
997                setTextCommon(view, R.id.when_time, whenTime + " (" + displayName + ")");
998            }
999            else {
1000                setTextCommon(view, R.id.when_time, whenTime);
1001            }
1002        }
1003
1004
1005        // Organizer view is setup in the updateCalendar method
1006
1007
1008        // Where
1009        if (location == null || location.trim().length() == 0) {
1010            setVisibilityCommon(view, R.id.where, View.GONE);
1011        } else {
1012            final TextView textView = mWhere;
1013            if (textView != null) {
1014                textView.setAutoLinkMask(0);
1015                textView.setText(location.trim());
1016                if (!Linkify.addLinks(textView, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES
1017                        | Linkify.MAP_ADDRESSES)) {
1018                    Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q=");
1019                }
1020                textView.setOnTouchListener(new OnTouchListener() {
1021                    @Override
1022                    public boolean onTouch(View v, MotionEvent event) {
1023                        try {
1024                            return v.onTouchEvent(event);
1025                        } catch (ActivityNotFoundException e) {
1026                            // ignore
1027                            return true;
1028                        }
1029                    }
1030                });
1031            }
1032        }
1033
1034        // Description
1035        if (description != null && description.length() != 0) {
1036            setTextCommon(view, R.id.description, description);
1037        }
1038        updateDescription ();  // Expand or collapse full description
1039    }
1040
1041    private void sendAccessibilityEvent() {
1042        AccessibilityManager am =
1043            (AccessibilityManager) getActivity().getSystemService(Service.ACCESSIBILITY_SERVICE);
1044        if (!am.isEnabled()) {
1045            return;
1046        }
1047
1048        AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
1049        event.setClassName(getClass().getName());
1050        event.setPackageName(getActivity().getPackageName());
1051        List<CharSequence> text = event.getText();
1052
1053        addFieldToAccessibilityEvent(text, mTitle);
1054        addFieldToAccessibilityEvent(text, mWhen);
1055        addFieldToAccessibilityEvent(text, mWhere);
1056        addFieldToAccessibilityEvent(text, mWhat);
1057        addFieldToAccessibilityEvent(text, mAttendees);
1058
1059        RadioGroup response = (RadioGroup) getView().findViewById(R.id.response_value);
1060        if (response.getVisibility() == View.VISIBLE) {
1061            int id = response.getCheckedRadioButtonId();
1062            if (id != View.NO_ID) {
1063                text.add(((TextView) getView().findViewById(R.id.response_label)).getText());
1064                text.add((((RadioButton) (response.findViewById(id))).getText() + PERIOD_SPACE));
1065            }
1066        }
1067
1068        am.sendAccessibilityEvent(event);
1069    }
1070
1071    /**
1072     * @param text
1073     */
1074    private void addFieldToAccessibilityEvent(List<CharSequence> text, TextView view) {
1075        if (view == null) {
1076            return;
1077        }
1078        String str = view.toString().trim();
1079        if (!TextUtils.isEmpty(str)) {
1080            text.add(mTitle.getText());
1081            text.add(PERIOD_SPACE);
1082        }
1083    }
1084
1085    private void updateCalendar(View view) {
1086        mCalendarOwnerAccount = "";
1087        if (mCalendarsCursor != null && mEventCursor != null) {
1088            mCalendarsCursor.moveToFirst();
1089            String tempAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
1090            mCalendarOwnerAccount = (tempAccount == null) ? "" : tempAccount;
1091            mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0;
1092
1093            String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
1094
1095            // start duplicate calendars query
1096            mHandler.startQuery(TOKEN_QUERY_DUPLICATE_CALENDARS, null, Calendars.CONTENT_URI,
1097                    CALENDARS_PROJECTION, CALENDARS_DUPLICATE_NAME_WHERE,
1098                    new String[] {displayName}, null);
1099
1100            String eventOrganizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER);
1101            mIsOrganizer = mCalendarOwnerAccount.equalsIgnoreCase(eventOrganizer);
1102            setTextCommon(view, R.id.organizer, eventOrganizer);
1103            if (!mIsOrganizer) {
1104                setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE);
1105            } else {
1106                setVisibilityCommon(view, R.id.organizer_container, View.GONE);
1107            }
1108            mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0;
1109            mCanModifyCalendar =
1110                    mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) >= Calendars.CAL_ACCESS_CONTRIBUTOR;
1111            mIsBusyFreeCalendar =
1112                    mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.CAL_ACCESS_FREEBUSY;
1113
1114            if (!mIsBusyFreeCalendar) {
1115                Button b = (Button) mView.findViewById(R.id.edit);
1116                b.setEnabled(true);
1117                b.setOnClickListener(new OnClickListener() {
1118                    @Override
1119                    public void onClick(View v) {
1120                        doEdit();
1121                        // For dialogs, just close the fragment
1122                        // For full screen, close activity on phone, leave it for tablet
1123                        if (mIsDialog) {
1124                            EventInfoFragment.this.dismiss();
1125                        }
1126                        else if (!mIsTabletConfig){
1127                            getActivity().finish();
1128                        }
1129                    }
1130                });
1131            }
1132            if (!mCanModifyCalendar) {
1133                if (mIsDialog) {
1134                    mView.findViewById(R.id.delete).setEnabled(false);
1135                }
1136                else {
1137                    MenuItem item = mMenu.findItem(R.id.info_action_delete);
1138                    if (item != null) {
1139                        item.setVisible(false);
1140                    }
1141                }
1142            }
1143        } else {
1144            setVisibilityCommon(view, R.id.calendar, View.GONE);
1145            sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS);
1146        }
1147    }
1148
1149    private void updateAttendees(View view) {
1150
1151        if (mAcceptedAttendees.size() + mDeclinedAttendees.size() +
1152                mTentativeAttendees.size() + mNoResponseAttendees.size() > 0) {
1153            (mLongAttendees).addAttendees(mAcceptedAttendees);
1154            (mLongAttendees).addAttendees(mDeclinedAttendees);
1155            (mLongAttendees).addAttendees(mTentativeAttendees);
1156            (mLongAttendees).addAttendees(mNoResponseAttendees);
1157            mLongAttendees.setEnabled(false);
1158            mLongAttendees.setVisibility(View.VISIBLE);
1159        } else {
1160            mLongAttendees.setVisibility(View.GONE);
1161        }
1162    }
1163
1164    public void initReminders(View view, Cursor cursor) {
1165
1166        // Add reminders
1167        while (cursor.moveToNext()) {
1168            int minutes = cursor.getInt(EditEventHelper.REMINDERS_INDEX_MINUTES);
1169            int method = cursor.getInt(EditEventHelper.REMINDERS_INDEX_METHOD);
1170            mOriginalReminders.add(ReminderEntry.valueOf(minutes, method));
1171        }
1172        // Sort appropriately for display (by time, then type)
1173        Collections.sort(mOriginalReminders);
1174
1175        // Load the labels and corresponding numeric values for the minutes and methods lists
1176        // from the assets.  If we're switching calendars, we need to clear and re-populate the
1177        // lists (which may have elements added and removed based on calendar properties).  This
1178        // is mostly relevant for "methods", since we shouldn't have any "minutes" values in a
1179        // new event that aren't in the default set.
1180        Resources r = mActivity.getResources();
1181        mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values);
1182        mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels);
1183        mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values);
1184        mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels);
1185
1186        // Remove any reminder methods that aren't allowed for this calendar.  If this is
1187        // a new event, mCalendarAllowedReminders may not be set the first time we're called.
1188        if (mCalendarAllowedReminders != null) {
1189            EventViewUtils.reduceMethodList(mReminderMethodValues, mReminderMethodLabels,
1190                    mCalendarAllowedReminders);
1191        }
1192
1193        int numReminders = 0;
1194        if (mHasAlarm) {
1195            ArrayList<ReminderEntry> reminders = mOriginalReminders;
1196            numReminders = reminders.size();
1197            // Insert any minute values that aren't represented in the minutes list.
1198            for (ReminderEntry re : reminders) {
1199                EventViewUtils.addMinutesToList(
1200                        mActivity, mReminderMinuteValues, mReminderMinuteLabels, re.getMinutes());
1201            }
1202            // Create a UI element for each reminder.  We display all of the reminders we get
1203            // from the provider, even if the count exceeds the calendar maximum.  (Also, for
1204            // a new event, we won't have a maxReminders value available.)
1205            for (ReminderEntry re : reminders) {
1206                      EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
1207                              mReminderMinuteValues, mReminderMinuteLabels,
1208                              mReminderMethodValues, mReminderMethodLabels,
1209                              re, Integer.MAX_VALUE);
1210            }
1211        }
1212    }
1213
1214    private void formatAttendees(ArrayList<Attendee> attendees, SpannableStringBuilder sb, int type) {
1215        if (attendees.size() <= 0) {
1216            return;
1217        }
1218
1219        int begin = sb.length();
1220        boolean firstTime = sb.length() == 0;
1221
1222        if (firstTime == false) {
1223            begin += 2; // skip over the ", " for formatting.
1224        }
1225
1226        for (Attendee attendee : attendees) {
1227            if (firstTime) {
1228                firstTime = false;
1229            } else {
1230                sb.append(", ");
1231            }
1232
1233            String name = attendee.getDisplayName();
1234            sb.append(name);
1235        }
1236
1237        switch (type) {
1238            case Attendees.ATTENDEE_STATUS_ACCEPTED:
1239                break;
1240            case Attendees.ATTENDEE_STATUS_DECLINED:
1241                sb.setSpan(new StrikethroughSpan(), begin, sb.length(),
1242                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1243                // fall through
1244            default:
1245                // The last INCLUSIVE causes the foreground color to be applied
1246                // to the rest of the span. If not, the comma at the end of the
1247                // declined or tentative may be black.
1248                sb.setSpan(new ForegroundColorSpan(0xFF999999), begin, sb.length(),
1249                        Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
1250                break;
1251        }
1252    }
1253
1254    void updateResponse(View view) {
1255        // we only let the user accept/reject/etc. a meeting if:
1256        // a) you can edit the event's containing calendar AND
1257        // b) you're not the organizer and only attendee AND
1258        // c) organizerCanRespond is enabled for the calendar
1259        // (if the attendee data has been hidden, the visible number of attendees
1260        // will be 1 -- the calendar owner's).
1261        // (there are more cases involved to be 100% accurate, such as
1262        // paying attention to whether or not an attendee status was
1263        // included in the feed, but we're currently omitting those corner cases
1264        // for simplicity).
1265
1266        // TODO Switch to EditEventHelper.canRespond when this class uses CalendarEventModel.
1267        if (!mCanModifyCalendar || (mHasAttendeeData && mIsOrganizer && mNumOfAttendees <= 1) ||
1268                (mIsOrganizer && !mOwnerCanRespond)) {
1269            setVisibilityCommon(view, R.id.response_container, View.GONE);
1270            return;
1271        }
1272
1273        setVisibilityCommon(view, R.id.response_container, View.VISIBLE);
1274
1275
1276        int response;
1277        if (mAttendeeResponseFromIntent != CalendarController.ATTENDEE_NO_RESPONSE) {
1278            response = mAttendeeResponseFromIntent;
1279        } else {
1280            response = mOriginalAttendeeResponse;
1281        }
1282
1283        int buttonToCheck = findButtonIdForResponse(response);
1284        RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.response_value);
1285        radioGroup.check(buttonToCheck); // -1 clear all radio buttons
1286        radioGroup.setOnCheckedChangeListener(this);
1287    }
1288
1289    private void setTextCommon(View view, int id, CharSequence text) {
1290        TextView textView = (TextView) view.findViewById(id);
1291        if (textView == null)
1292            return;
1293        textView.setText(text);
1294    }
1295
1296    private void setVisibilityCommon(View view, int id, int visibility) {
1297        View v = view.findViewById(id);
1298        if (v != null) {
1299            v.setVisibility(visibility);
1300        }
1301        return;
1302    }
1303
1304    /**
1305     * Taken from com.google.android.gm.HtmlConversationActivity
1306     *
1307     * Send the intent that shows the Contact info corresponding to the email address.
1308     */
1309    public void showContactInfo(Attendee attendee, Rect rect) {
1310        // First perform lookup query to find existing contact
1311        final ContentResolver resolver = getActivity().getContentResolver();
1312        final String address = attendee.mEmail;
1313        final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI,
1314                Uri.encode(address));
1315        final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri);
1316
1317        if (lookupUri != null) {
1318            // Found matching contact, trigger QuickContact
1319            QuickContact.showQuickContact(getActivity(), rect, lookupUri,
1320                    QuickContact.MODE_MEDIUM, null);
1321        } else {
1322            // No matching contact, ask user to create one
1323            final Uri mailUri = Uri.fromParts("mailto", address, null);
1324            final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, mailUri);
1325
1326            // Pass along full E-mail string for possible create dialog
1327            Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null);
1328            intent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION, sender.toString());
1329
1330            // Only provide personal name hint if we have one
1331            final String senderPersonal = attendee.mName;
1332            if (!TextUtils.isEmpty(senderPersonal)) {
1333                intent.putExtra(Intents.Insert.NAME, senderPersonal);
1334            }
1335
1336            startActivity(intent);
1337        }
1338    }
1339
1340    @Override
1341    public void onPause() {
1342        mIsPaused = true;
1343        mHandler.removeCallbacks(onDeleteRunnable);
1344        super.onPause();
1345    }
1346
1347    @Override
1348    public void onResume() {
1349        super.onResume();
1350        mIsPaused = false;
1351        if (mDismissOnResume) {
1352            mHandler.post(onDeleteRunnable);
1353        }
1354    }
1355
1356    @Override
1357    public void eventsChanged() {
1358    }
1359
1360    @Override
1361    public long getSupportedEventTypes() {
1362        return EventType.EVENTS_CHANGED;
1363    }
1364
1365    @Override
1366    public void handleEvent(EventInfo event) {
1367        if (event.eventType == EventType.EVENTS_CHANGED) {
1368            // reload the data
1369            mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION,
1370                    null, null, null);
1371        }
1372
1373    }
1374
1375
1376    @Override
1377    public void onClick(View view) {
1378
1379        // This must be a click on one of the "remove reminder" buttons
1380        LinearLayout reminderItem = (LinearLayout) view.getParent();
1381        LinearLayout parent = (LinearLayout) reminderItem.getParent();
1382        parent.removeView(reminderItem);
1383        mReminderViews.remove(reminderItem);
1384    }
1385
1386
1387    /**
1388     * Add a new reminder when the user hits the "add reminder" button.  We use the default
1389     * reminder time and method.
1390     */
1391    private void addReminder() {
1392        // TODO: when adding a new reminder, make it different from the
1393        // last one in the list (if any).
1394        if (mDefaultReminderMinutes == GeneralPreferences.NO_REMINDER) {
1395            EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
1396                    mReminderMinuteValues, mReminderMinuteLabels,
1397                    mReminderMethodValues, mReminderMethodLabels,
1398                    ReminderEntry.valueOf(GeneralPreferences.REMINDER_DEFAULT_TIME),
1399                    mMaxReminders);
1400        } else {
1401            EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
1402                    mReminderMinuteValues, mReminderMinuteLabels,
1403                    mReminderMethodValues, mReminderMethodLabels,
1404                    ReminderEntry.valueOf(mDefaultReminderMinutes),
1405                    mMaxReminders);
1406        }
1407    }
1408
1409
1410    private void prepareReminders() {
1411        Resources r = mActivity.getResources();
1412        mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values);
1413        mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels);
1414        mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values);
1415        mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels);
1416    }
1417
1418
1419    private boolean saveReminders() {
1420        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(3);
1421
1422        // Read reminders from UI
1423        mReminders = EventViewUtils.reminderItemsToReminders(mReminderViews,
1424                mReminderMinuteValues, mReminderMethodValues);
1425
1426        // Check if there are any changes in the reminder
1427        boolean changed = EditEventHelper.saveReminders(ops, mEventId, mReminders,
1428                mOriginalReminders, false /* no force save */);
1429
1430        if (!changed) {
1431            return false;
1432        }
1433
1434        // save new reminders
1435        AsyncQueryService service = new AsyncQueryService(getActivity());
1436        service.startBatch(0, null, Calendars.CONTENT_URI.getAuthority(), ops, 0);
1437        // Update the "hasAlarm" field for the event
1438        Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
1439        int len = mReminders.size();
1440        boolean hasAlarm = len > 0;
1441        if (hasAlarm != mHasAlarm) {
1442            ContentValues values = new ContentValues();
1443            values.put(Events.HAS_ALARM, hasAlarm ? 1 : 0);
1444            service.startUpdate(0, null, uri, values, null, null, 0);
1445        }
1446        return true;
1447    }
1448
1449    /**
1450     * Loads an integer array asset into a list.
1451     */
1452    private static ArrayList<Integer> loadIntegerArray(Resources r, int resNum) {
1453        int[] vals = r.getIntArray(resNum);
1454        int size = vals.length;
1455        ArrayList<Integer> list = new ArrayList<Integer>(size);
1456
1457        for (int i = 0; i < size; i++) {
1458            list.add(vals[i]);
1459        }
1460
1461        return list;
1462    }
1463    /**
1464     * Loads a String array asset into a list.
1465     */
1466    private static ArrayList<String> loadStringArray(Resources r, int resNum) {
1467        String[] labels = r.getStringArray(resNum);
1468        ArrayList<String> list = new ArrayList<String>(Arrays.asList(labels));
1469        return list;
1470    }
1471
1472}
1473