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