EventInfoFragment.java revision 80d640f3edc455d053a0e71a419f222a6f03385e
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.EditEventActivity;
25import com.android.calendar.event.EditEventHelper;
26import com.android.calendar.event.EventViewUtils;
27import com.android.calendarcommon.EventRecurrence;
28
29import android.animation.Animator;
30import android.animation.AnimatorListenerAdapter;
31import android.animation.ObjectAnimator;
32import android.app.Activity;
33import android.app.Dialog;
34import android.app.DialogFragment;
35import android.app.Service;
36import android.content.ActivityNotFoundException;
37import android.content.ContentProviderOperation;
38import android.content.ContentResolver;
39import android.content.ContentUris;
40import android.content.ContentValues;
41import android.content.Context;
42import android.content.DialogInterface;
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.SpannableString;
62import android.text.SpannableStringBuilder;
63import android.text.Spanned;
64import android.text.TextUtils;
65import android.text.format.Time;
66import android.text.method.LinkMovementMethod;
67import android.text.method.MovementMethod;
68import android.text.style.ForegroundColorSpan;
69import android.text.style.StyleSpan;
70import android.text.style.URLSpan;
71import android.text.util.Linkify;
72import android.text.util.Rfc822Token;
73import android.util.Log;
74import android.view.Gravity;
75import android.view.LayoutInflater;
76import android.view.Menu;
77import android.view.MenuInflater;
78import android.view.MenuItem;
79import android.view.MotionEvent;
80import android.view.View;
81import android.view.View.OnClickListener;
82import android.view.View.OnTouchListener;
83import android.view.ViewGroup;
84import android.view.Window;
85import android.view.WindowManager;
86import android.view.accessibility.AccessibilityEvent;
87import android.view.accessibility.AccessibilityManager;
88import android.widget.AdapterView;
89import android.widget.AdapterView.OnItemSelectedListener;
90import android.widget.Button;
91import android.widget.LinearLayout;
92import android.widget.RadioButton;
93import android.widget.RadioGroup;
94import android.widget.RadioGroup.OnCheckedChangeListener;
95import android.widget.ScrollView;
96import android.widget.TextView;
97import android.widget.Toast;
98
99import java.util.ArrayList;
100import java.util.Arrays;
101import java.util.Collections;
102import java.util.List;
103import java.util.regex.Pattern;
104
105import static android.provider.CalendarContract.EXTRA_EVENT_ALL_DAY;
106import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME;
107import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME;
108import static com.android.calendar.CalendarController.EVENT_EDIT_ON_LAUNCH;
109
110
111public class EventInfoFragment extends DialogFragment implements OnCheckedChangeListener,
112        CalendarController.EventHandler, OnClickListener, DeleteEventHelper.DeleteNotifyListener {
113    public static final boolean DEBUG = false;
114
115    public static final String TAG = "EventInfoFragment";
116
117    protected static final String BUNDLE_KEY_EVENT_ID = "key_event_id";
118    protected static final String BUNDLE_KEY_START_MILLIS = "key_start_millis";
119    protected static final String BUNDLE_KEY_END_MILLIS = "key_end_millis";
120    protected static final String BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog";
121    protected static final String BUNDLE_KEY_DELETE_DIALOG_VISIBLE = "key_delete_dialog_visible";
122    protected static final String BUNDLE_KEY_WINDOW_STYLE = "key_window_style";
123    protected static final String BUNDLE_KEY_ATTENDEE_RESPONSE = "key_attendee_response";
124
125    private static final String PERIOD_SPACE = ". ";
126
127    /**
128     * These are the corresponding indices into the array of strings
129     * "R.array.change_response_labels" in the resource file.
130     */
131    static final int UPDATE_SINGLE = 0;
132    static final int UPDATE_ALL = 1;
133
134    // Style of view
135    public static final int FULL_WINDOW_STYLE = 0;
136    public static final int DIALOG_WINDOW_STYLE = 1;
137
138    private int mWindowStyle = DIALOG_WINDOW_STYLE;
139
140    // Query tokens for QueryHandler
141    private static final int TOKEN_QUERY_EVENT = 1 << 0;
142    private static final int TOKEN_QUERY_CALENDARS = 1 << 1;
143    private static final int TOKEN_QUERY_ATTENDEES = 1 << 2;
144    private static final int TOKEN_QUERY_DUPLICATE_CALENDARS = 1 << 3;
145    private static final int TOKEN_QUERY_REMINDERS = 1 << 4;
146    private static final int TOKEN_QUERY_ALL = TOKEN_QUERY_DUPLICATE_CALENDARS
147            | TOKEN_QUERY_ATTENDEES | TOKEN_QUERY_CALENDARS | TOKEN_QUERY_EVENT
148            | TOKEN_QUERY_REMINDERS;
149    private int mCurrentQuery = 0;
150
151    private static final String[] EVENT_PROJECTION = new String[] {
152        Events._ID,                  // 0  do not remove; used in DeleteEventHelper
153        Events.TITLE,                // 1  do not remove; used in DeleteEventHelper
154        Events.RRULE,                // 2  do not remove; used in DeleteEventHelper
155        Events.ALL_DAY,              // 3  do not remove; used in DeleteEventHelper
156        Events.CALENDAR_ID,          // 4  do not remove; used in DeleteEventHelper
157        Events.DTSTART,              // 5  do not remove; used in DeleteEventHelper
158        Events._SYNC_ID,             // 6  do not remove; used in DeleteEventHelper
159        Events.EVENT_TIMEZONE,       // 7  do not remove; used in DeleteEventHelper
160        Events.DESCRIPTION,          // 8
161        Events.EVENT_LOCATION,       // 9
162        Calendars.CALENDAR_ACCESS_LEVEL,      // 10
163        Events.DISPLAY_COLOR,                 // 11
164        Events.HAS_ATTENDEE_DATA,    // 12
165        Events.ORGANIZER,            // 13
166        Events.HAS_ALARM,            // 14
167        Calendars.MAX_REMINDERS,     //15
168        Calendars.ALLOWED_REMINDERS, // 16
169        Events.ORIGINAL_SYNC_ID,     // 17 do not remove; used in DeleteEventHelper
170    };
171    private static final int EVENT_INDEX_ID = 0;
172    private static final int EVENT_INDEX_TITLE = 1;
173    private static final int EVENT_INDEX_RRULE = 2;
174    private static final int EVENT_INDEX_ALL_DAY = 3;
175    private static final int EVENT_INDEX_CALENDAR_ID = 4;
176    private static final int EVENT_INDEX_SYNC_ID = 6;
177    private static final int EVENT_INDEX_EVENT_TIMEZONE = 7;
178    private static final int EVENT_INDEX_DESCRIPTION = 8;
179    private static final int EVENT_INDEX_EVENT_LOCATION = 9;
180    private static final int EVENT_INDEX_ACCESS_LEVEL = 10;
181    private static final int EVENT_INDEX_COLOR = 11;
182    private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 12;
183    private static final int EVENT_INDEX_ORGANIZER = 13;
184    private static final int EVENT_INDEX_HAS_ALARM = 14;
185    private static final int EVENT_INDEX_MAX_REMINDERS = 15;
186    private static final int EVENT_INDEX_ALLOWED_REMINDERS = 16;
187
188
189    private static final String[] ATTENDEES_PROJECTION = new String[] {
190        Attendees._ID,                      // 0
191        Attendees.ATTENDEE_NAME,            // 1
192        Attendees.ATTENDEE_EMAIL,           // 2
193        Attendees.ATTENDEE_RELATIONSHIP,    // 3
194        Attendees.ATTENDEE_STATUS,          // 4
195    };
196    private static final int ATTENDEES_INDEX_ID = 0;
197    private static final int ATTENDEES_INDEX_NAME = 1;
198    private static final int ATTENDEES_INDEX_EMAIL = 2;
199    private static final int ATTENDEES_INDEX_RELATIONSHIP = 3;
200    private static final int ATTENDEES_INDEX_STATUS = 4;
201
202    private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?";
203
204    private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, "
205            + Attendees.ATTENDEE_EMAIL + " ASC";
206
207    private static final String[] REMINDERS_PROJECTION = new String[] {
208        Reminders._ID,                      // 0
209        Reminders.MINUTES,            // 1
210        Reminders.METHOD           // 2
211    };
212    private static final int REMINDERS_INDEX_ID = 0;
213    private static final int REMINDERS_MINUTES_ID = 1;
214    private static final int REMINDERS_METHOD_ID = 2;
215
216    private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=?";
217
218    static final String[] CALENDARS_PROJECTION = new String[] {
219        Calendars._ID,           // 0
220        Calendars.CALENDAR_DISPLAY_NAME,  // 1
221        Calendars.OWNER_ACCOUNT, // 2
222        Calendars.CAN_ORGANIZER_RESPOND, // 3
223        Calendars.ACCOUNT_NAME // 4
224    };
225    static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
226    static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
227    static final int CALENDARS_INDEX_OWNER_CAN_RESPOND = 3;
228    static final int CALENDARS_INDEX_ACCOUNT_NAME = 4;
229
230    static final String CALENDARS_WHERE = Calendars._ID + "=?";
231    static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.CALENDAR_DISPLAY_NAME + "=?";
232
233    private static final String NANP_ALLOWED_SYMBOLS = "()+-*#.";
234    private static final int NANP_MIN_DIGITS = 7;
235    private static final int NANP_MAX_DIGITS = 11;
236
237
238    private View mView;
239
240    private Uri mUri;
241    private long mEventId;
242    private Cursor mEventCursor;
243    private Cursor mAttendeesCursor;
244    private Cursor mCalendarsCursor;
245    private Cursor mRemindersCursor;
246
247    private static float mScale = 0; // Used for supporting different screen densities
248
249    private long mStartMillis;
250    private long mEndMillis;
251    private boolean mAllDay;
252
253    private boolean mHasAttendeeData;
254    private String mEventOrganizer;
255    private boolean mIsOrganizer;
256    private long mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE;
257    private boolean mOwnerCanRespond;
258    private String mSyncAccountName;
259    private String mCalendarOwnerAccount;
260    private boolean mCanModifyCalendar;
261    private boolean mCanModifyEvent;
262    private boolean mIsBusyFreeCalendar;
263    private int mNumOfAttendees;
264
265    private EditResponseHelper mEditResponseHelper;
266    private boolean mDeleteDialogVisible = false;
267    private DeleteEventHelper mDeleteHelper;
268
269    private int mOriginalAttendeeResponse;
270    private int mAttendeeResponseFromIntent = CalendarController.ATTENDEE_NO_RESPONSE;
271    private int mUserSetResponse = CalendarController.ATTENDEE_NO_RESPONSE;
272    private boolean mIsRepeating;
273    private boolean mHasAlarm;
274    private int mMaxReminders;
275    private String mCalendarAllowedReminders;
276    // Used to prevent saving changes in event if it is being deleted.
277    private boolean mEventDeletionStarted = false;
278
279    private TextView mTitle;
280    private TextView mWhenDateTime;
281    private TextView mWhere;
282    private ExpandableTextView mDesc;
283    private AttendeesView mLongAttendees;
284    private Menu mMenu = null;
285    private View mHeadlines;
286    private ScrollView mScrollView;
287    private View mLoadingMsgView;
288    private ObjectAnimator mAnimateAlpha;
289    private long mLoadingMsgStartTime;
290    private static final int FADE_IN_TIME = 300;   // in milliseconds
291    private static final int LOADING_MSG_DELAY = 600;   // in milliseconds
292    private static final int LOADING_MSG_MIN_DISPLAY_TIME = 600;
293    private boolean mNoCrossFade = false;  // Used to prevent repeated cross-fade
294
295
296    private static final Pattern mWildcardPattern = Pattern.compile("^.*$");
297
298    ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>();
299    ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>();
300    ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>();
301    ArrayList<Attendee> mNoResponseAttendees = new ArrayList<Attendee>();
302    ArrayList<String> mToEmails = new ArrayList<String>();
303    ArrayList<String> mCcEmails = new ArrayList<String>();
304    private int mColor;
305
306
307    private int mDefaultReminderMinutes;
308    private final ArrayList<LinearLayout> mReminderViews = new ArrayList<LinearLayout>(0);
309    public ArrayList<ReminderEntry> mReminders;
310    public ArrayList<ReminderEntry> mOriginalReminders = new ArrayList<ReminderEntry>();
311    public ArrayList<ReminderEntry> mUnsupportedReminders = new ArrayList<ReminderEntry>();
312    private boolean mUserModifiedReminders = false;
313
314    /**
315     * Contents of the "minutes" spinner.  This has default values from the XML file, augmented
316     * with any additional values that were already associated with the event.
317     */
318    private ArrayList<Integer> mReminderMinuteValues;
319    private ArrayList<String> mReminderMinuteLabels;
320
321    /**
322     * Contents of the "methods" spinner.  The "values" list specifies the method constant
323     * (e.g. {@link Reminders#METHOD_ALERT}) associated with the labels.  Any methods that
324     * aren't allowed by the Calendar will be removed.
325     */
326    private ArrayList<Integer> mReminderMethodValues;
327    private ArrayList<String> mReminderMethodLabels;
328
329    private QueryHandler mHandler;
330
331
332    private final Runnable mTZUpdater = new Runnable() {
333        @Override
334        public void run() {
335            updateEvent(mView);
336        }
337    };
338
339    private final Runnable mLoadingMsgAlphaUpdater = new Runnable() {
340        @Override
341        public void run() {
342            // Since this is run after a delay, make sure to only show the message
343            // if the event's data is not shown yet.
344            if (!mAnimateAlpha.isRunning() && mScrollView.getAlpha() == 0) {
345                mLoadingMsgStartTime = System.currentTimeMillis();
346                mLoadingMsgView.setAlpha(1);
347            }
348        }
349    };
350
351    private OnItemSelectedListener mReminderChangeListener;
352
353    private static int mDialogWidth = 500;
354    private static int mDialogHeight = 600;
355    private static int DIALOG_TOP_MARGIN = 8;
356    private boolean mIsDialog = false;
357    private boolean mIsPaused = true;
358    private boolean mDismissOnResume = false;
359    private int mX = -1;
360    private int mY = -1;
361    private int mMinTop;         // Dialog cannot be above this location
362    private boolean mIsTabletConfig;
363    private Activity mActivity;
364    private Context mContext;
365
366    private class QueryHandler extends AsyncQueryService {
367        public QueryHandler(Context context) {
368            super(context);
369        }
370
371        @Override
372        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
373            // if the activity is finishing, then close the cursor and return
374            final Activity activity = getActivity();
375            if (activity == null || activity.isFinishing()) {
376                cursor.close();
377                return;
378            }
379
380            switch (token) {
381            case TOKEN_QUERY_EVENT:
382                mEventCursor = Utils.matrixCursorFromCursor(cursor);
383                if (initEventCursor()) {
384                    // The cursor is empty. This can happen if the event was
385                    // deleted.
386                    // FRAG_TODO we should no longer rely on Activity.finish()
387                    activity.finish();
388                    return;
389                }
390                updateEvent(mView);
391                prepareReminders();
392
393                // start calendar query
394                Uri uri = Calendars.CONTENT_URI;
395                String[] args = new String[] {
396                        Long.toString(mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID))};
397                startQuery(TOKEN_QUERY_CALENDARS, null, uri, CALENDARS_PROJECTION,
398                        CALENDARS_WHERE, args, null);
399                break;
400            case TOKEN_QUERY_CALENDARS:
401                mCalendarsCursor = Utils.matrixCursorFromCursor(cursor);
402                updateCalendar(mView);
403                // FRAG_TODO fragments shouldn't set the title anymore
404                updateTitle();
405
406                if (!mIsBusyFreeCalendar) {
407                    args = new String[] { Long.toString(mEventId) };
408
409                    // start attendees query
410                    uri = Attendees.CONTENT_URI;
411                    startQuery(TOKEN_QUERY_ATTENDEES, null, uri, ATTENDEES_PROJECTION,
412                            ATTENDEES_WHERE, args, ATTENDEES_SORT_ORDER);
413                } else {
414                    sendAccessibilityEventIfQueryDone(TOKEN_QUERY_ATTENDEES);
415                }
416                if (mHasAlarm) {
417                    // start reminders query
418                    args = new String[] { Long.toString(mEventId) };
419                    uri = Reminders.CONTENT_URI;
420                    startQuery(TOKEN_QUERY_REMINDERS, null, uri,
421                            REMINDERS_PROJECTION, REMINDERS_WHERE, args, null);
422                } else {
423                    sendAccessibilityEventIfQueryDone(TOKEN_QUERY_REMINDERS);
424                }
425                break;
426            case TOKEN_QUERY_ATTENDEES:
427                mAttendeesCursor = Utils.matrixCursorFromCursor(cursor);
428                initAttendeesCursor(mView);
429                updateResponse(mView);
430                break;
431            case TOKEN_QUERY_REMINDERS:
432                mRemindersCursor = Utils.matrixCursorFromCursor(cursor);
433                initReminders(mView, mRemindersCursor);
434                break;
435            case TOKEN_QUERY_DUPLICATE_CALENDARS:
436                Resources res = activity.getResources();
437                SpannableStringBuilder sb = new SpannableStringBuilder();
438
439                // Label
440                String label = res.getString(R.string.view_event_calendar_label);
441                sb.append(label).append(" ");
442                sb.setSpan(new StyleSpan(Typeface.BOLD), 0, label.length(),
443                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
444
445                // Calendar display name
446                String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
447                sb.append(calendarName);
448
449                // Show email account if display name is not unique and
450                // display name != email
451                String email = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
452                if (cursor.getCount() > 1 && !calendarName.equalsIgnoreCase(email)) {
453                    sb.append(" (").append(email).append(")");
454                }
455
456                break;
457            }
458            cursor.close();
459            sendAccessibilityEventIfQueryDone(token);
460            // All queries are done, show the view
461            if (mCurrentQuery == TOKEN_QUERY_ALL) {
462                if (mLoadingMsgView.getAlpha() == 1) {
463                    // Loading message is showing, let it stay a bit more (to prevent
464                    // flashing) by adding a start delay to the event animation
465                    long timeDiff = LOADING_MSG_MIN_DISPLAY_TIME - (System.currentTimeMillis() -
466                            mLoadingMsgStartTime);
467                    if (timeDiff > 0) {
468                        mAnimateAlpha.setStartDelay(timeDiff);
469                    }
470                }
471                if (!mAnimateAlpha.isRunning() &&!mAnimateAlpha.isStarted() && !mNoCrossFade) {
472                    mAnimateAlpha.start();
473                } else {
474                    mScrollView.setAlpha(1);
475                    mLoadingMsgView.setVisibility(View.GONE);
476                }
477            }
478        }
479    }
480
481    private void sendAccessibilityEventIfQueryDone(int token) {
482        mCurrentQuery |= token;
483        if (mCurrentQuery == TOKEN_QUERY_ALL) {
484            sendAccessibilityEvent();
485        }
486    }
487
488    public EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis,
489            int attendeeResponse, boolean isDialog, int windowStyle) {
490
491        if (isDialog) {
492            Resources r = context.getResources();
493
494            mDialogWidth = r.getInteger(R.integer.event_info_dialog_width);
495            mDialogHeight = r.getInteger(R.integer.event_info_dialog_height);
496
497            if (mScale == 0) {
498                mScale = context.getResources().getDisplayMetrics().density;
499                if (mScale != 1) {
500                    mDialogWidth *= mScale;
501                    mDialogHeight *= mScale;
502                    DIALOG_TOP_MARGIN *= mScale;
503                }
504            }
505        }
506        mIsDialog = isDialog;
507
508        setStyle(DialogFragment.STYLE_NO_TITLE, 0);
509        mUri = uri;
510        mStartMillis = startMillis;
511        mEndMillis = endMillis;
512        mAttendeeResponseFromIntent = attendeeResponse;
513        mWindowStyle = windowStyle;
514    }
515
516    // This is currently required by the fragment manager.
517    public EventInfoFragment() {
518    }
519
520
521
522    public EventInfoFragment(Context context, long eventId, long startMillis, long endMillis,
523            int attendeeResponse, boolean isDialog, int windowStyle) {
524        this(context, ContentUris.withAppendedId(Events.CONTENT_URI, eventId), startMillis,
525                endMillis, attendeeResponse, isDialog, windowStyle);
526        mEventId = eventId;
527    }
528
529    @Override
530    public void onActivityCreated(Bundle savedInstanceState) {
531        super.onActivityCreated(savedInstanceState);
532
533        mReminderChangeListener = new OnItemSelectedListener() {
534            @Override
535            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
536                Integer prevValue = (Integer) parent.getTag();
537                if (prevValue == null || prevValue != position) {
538                    parent.setTag(position);
539                    mUserModifiedReminders = true;
540                }
541            }
542
543            @Override
544            public void onNothingSelected(AdapterView<?> parent) {
545                // do nothing
546            }
547
548        };
549
550        if (savedInstanceState != null) {
551            mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false);
552            mWindowStyle = savedInstanceState.getInt(BUNDLE_KEY_WINDOW_STYLE,
553                    DIALOG_WINDOW_STYLE);
554        }
555
556        if (mIsDialog) {
557            applyDialogParams();
558        }
559        mContext = getActivity();
560    }
561
562    private void applyDialogParams() {
563        Dialog dialog = getDialog();
564        dialog.setCanceledOnTouchOutside(true);
565
566        Window window = dialog.getWindow();
567        window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
568
569        WindowManager.LayoutParams a = window.getAttributes();
570        a.dimAmount = .4f;
571
572        a.width = mDialogWidth;
573        a.height = mDialogHeight;
574
575
576        // On tablets , do smart positioning of dialog
577        // On phones , use the whole screen
578
579        if (mX != -1 || mY != -1) {
580            a.x = mX - mDialogWidth / 2;
581            a.y = mY - mDialogHeight / 2;
582            if (a.y < mMinTop) {
583                a.y = mMinTop + DIALOG_TOP_MARGIN;
584            }
585            a.gravity = Gravity.LEFT | Gravity.TOP;
586        }
587        window.setAttributes(a);
588    }
589
590    public void setDialogParams(int x, int y, int minTop) {
591        mX = x;
592        mY = y;
593        mMinTop = minTop;
594    }
595
596    // Implements OnCheckedChangeListener
597    @Override
598    public void onCheckedChanged(RadioGroup group, int checkedId) {
599        // If this is not a repeating event, then don't display the dialog
600        // asking which events to change.
601        mUserSetResponse = getResponseFromButtonId(checkedId);
602        if (!mIsRepeating) {
603            return;
604        }
605
606        // If the selection is the same as the original, then don't display the
607        // dialog asking which events to change.
608        if (checkedId == findButtonIdForResponse(mOriginalAttendeeResponse)) {
609            return;
610        }
611
612        // This is a repeating event. We need to ask the user if they mean to
613        // change just this one instance or all instances.
614        mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents());
615    }
616
617    public void onNothingSelected(AdapterView<?> parent) {
618    }
619
620    @Override
621    public void onAttach(Activity activity) {
622        super.onAttach(activity);
623        mActivity = activity;
624        mEditResponseHelper = new EditResponseHelper(activity);
625
626        if (mAttendeeResponseFromIntent != Attendees.ATTENDEE_STATUS_NONE) {
627            mEditResponseHelper.setWhichEvents(UPDATE_ALL);
628        }
629        mHandler = new QueryHandler(activity);
630        if (!mIsDialog) {
631            setHasOptionsMenu(true);
632        }
633    }
634
635    @Override
636    public View onCreateView(LayoutInflater inflater, ViewGroup container,
637            Bundle savedInstanceState) {
638
639        if (savedInstanceState != null) {
640            mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false);
641            mWindowStyle = savedInstanceState.getInt(BUNDLE_KEY_WINDOW_STYLE,
642                    DIALOG_WINDOW_STYLE);
643            mDeleteDialogVisible =
644                savedInstanceState.getBoolean(BUNDLE_KEY_DELETE_DIALOG_VISIBLE,false);
645
646        }
647
648        if (mWindowStyle == DIALOG_WINDOW_STYLE) {
649            mView = inflater.inflate(R.layout.event_info_dialog, container, false);
650        } else {
651            mView = inflater.inflate(R.layout.event_info, container, false);
652        }
653        mScrollView = (ScrollView) mView.findViewById(R.id.event_info_scroll_view);
654        mLoadingMsgView = mView.findViewById(R.id.event_info_loading_msg);
655        mTitle = (TextView) mView.findViewById(R.id.title);
656        mWhenDateTime = (TextView) mView.findViewById(R.id.when_datetime);
657        mWhere = (TextView) mView.findViewById(R.id.where);
658        mDesc = (ExpandableTextView) mView.findViewById(R.id.description);
659        mHeadlines = mView.findViewById(R.id.event_info_headline);
660        mLongAttendees = (AttendeesView)mView.findViewById(R.id.long_attendee_list);
661        mIsTabletConfig = Utils.getConfigBool(mActivity, R.bool.tablet_config);
662
663        if (mUri == null) {
664            // restore event ID from bundle
665            mEventId = savedInstanceState.getLong(BUNDLE_KEY_EVENT_ID);
666            mUri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
667            mStartMillis = savedInstanceState.getLong(BUNDLE_KEY_START_MILLIS);
668            mEndMillis = savedInstanceState.getLong(BUNDLE_KEY_END_MILLIS);
669        }
670
671        mAnimateAlpha = ObjectAnimator.ofFloat(mScrollView, "Alpha", 0, 1);
672        mAnimateAlpha.setDuration(FADE_IN_TIME);
673        mAnimateAlpha.addListener(new AnimatorListenerAdapter() {
674            int defLayerType;
675
676            @Override
677            public void onAnimationStart(Animator animation) {
678                // Use hardware layer for better performance during animation
679                defLayerType = mScrollView.getLayerType();
680                mScrollView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
681                // Ensure that the loading message is gone before showing the
682                // event info
683                mLoadingMsgView.removeCallbacks(mLoadingMsgAlphaUpdater);
684                mLoadingMsgView.setVisibility(View.GONE);
685            }
686
687            @Override
688            public void onAnimationCancel(Animator animation) {
689                mScrollView.setLayerType(defLayerType, null);
690            }
691
692            @Override
693            public void onAnimationEnd(Animator animation) {
694                mScrollView.setLayerType(defLayerType, null);
695                // Do not cross fade after the first time
696                mNoCrossFade = true;
697            }
698        });
699
700        mLoadingMsgView.setAlpha(0);
701        mScrollView.setAlpha(0);
702        mLoadingMsgView.postDelayed(mLoadingMsgAlphaUpdater, LOADING_MSG_DELAY);
703
704        // start loading the data
705
706        mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION,
707                null, null, null);
708
709        Button b = (Button) mView.findViewById(R.id.delete);
710        b.setOnClickListener(new OnClickListener() {
711            @Override
712            public void onClick(View v) {
713                if (!mCanModifyCalendar) {
714                    return;
715                }
716                mDeleteHelper = new DeleteEventHelper(
717                        mContext, mActivity,
718                        !mIsDialog && !mIsTabletConfig /* exitWhenDone */);
719                mDeleteHelper.setDeleteNotificationListener(EventInfoFragment.this);
720                mDeleteHelper.setOnDismissListener(createDeleteOnDismissListener());
721                mDeleteDialogVisible = true;
722                mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable);
723            }});
724
725        // Hide Edit/Delete buttons if in full screen mode on a phone
726        if (!mIsDialog && !mIsTabletConfig || mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) {
727            mView.findViewById(R.id.event_info_buttons_container).setVisibility(View.GONE);
728        }
729
730        // Create a listener for the email guests button
731        View emailAttendeesButton = mView.findViewById(R.id.email_attendees_button);
732        if (emailAttendeesButton != null) {
733            emailAttendeesButton.setOnClickListener(new View.OnClickListener() {
734                @Override
735                public void onClick(View v) {
736                    emailAttendees();
737                }
738            });
739        }
740
741        // Create a listener for the add reminder button
742        View reminderAddButton = mView.findViewById(R.id.reminder_add);
743        View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
744            @Override
745            public void onClick(View v) {
746                addReminder();
747                mUserModifiedReminders = true;
748            }
749        };
750        reminderAddButton.setOnClickListener(addReminderOnClickListener);
751
752        // Set reminders variables
753
754        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mActivity);
755        String defaultReminderString = prefs.getString(
756                GeneralPreferences.KEY_DEFAULT_REMINDER, GeneralPreferences.NO_REMINDER_STRING);
757        mDefaultReminderMinutes = Integer.parseInt(defaultReminderString);
758        prepareReminders();
759
760        return mView;
761    }
762
763    private final Runnable onDeleteRunnable = new Runnable() {
764        @Override
765        public void run() {
766            if (EventInfoFragment.this.mIsPaused) {
767                mDismissOnResume = true;
768                return;
769            }
770            if (EventInfoFragment.this.isVisible()) {
771                EventInfoFragment.this.dismiss();
772            }
773        }
774    };
775
776    private void updateTitle() {
777        Resources res = getActivity().getResources();
778        if (mCanModifyCalendar && !mIsOrganizer) {
779            getActivity().setTitle(res.getString(R.string.event_info_title_invite));
780        } else {
781            getActivity().setTitle(res.getString(R.string.event_info_title));
782        }
783    }
784
785    /**
786     * Initializes the event cursor, which is expected to point to the first
787     * (and only) result from a query.
788     * @return true if the cursor is empty.
789     */
790    private boolean initEventCursor() {
791        if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) {
792            return true;
793        }
794        mEventCursor.moveToFirst();
795        mEventId = mEventCursor.getInt(EVENT_INDEX_ID);
796        String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
797        mIsRepeating = !TextUtils.isEmpty(rRule);
798        mHasAlarm = (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) == 1)?true:false;
799        mMaxReminders = mEventCursor.getInt(EVENT_INDEX_MAX_REMINDERS);
800        mCalendarAllowedReminders =  mEventCursor.getString(EVENT_INDEX_ALLOWED_REMINDERS);
801        return false;
802    }
803
804    @SuppressWarnings("fallthrough")
805    private void initAttendeesCursor(View view) {
806        mOriginalAttendeeResponse = CalendarController.ATTENDEE_NO_RESPONSE;
807        mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE;
808        mNumOfAttendees = 0;
809        if (mAttendeesCursor != null) {
810            mNumOfAttendees = mAttendeesCursor.getCount();
811            if (mAttendeesCursor.moveToFirst()) {
812                mAcceptedAttendees.clear();
813                mDeclinedAttendees.clear();
814                mTentativeAttendees.clear();
815                mNoResponseAttendees.clear();
816
817                do {
818                    int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
819                    String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME);
820                    String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
821
822                    if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE &&
823                            mCalendarOwnerAccount.equalsIgnoreCase(email)) {
824                        mCalendarOwnerAttendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID);
825                        mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
826                    } else {
827                        // Don't show your own status in the list because:
828                        //  1) it doesn't make sense for event without other guests.
829                        //  2) there's a spinner for that for events with guests.
830                        switch(status) {
831                            case Attendees.ATTENDEE_STATUS_ACCEPTED:
832                                mAcceptedAttendees.add(new Attendee(name, email,
833                                        Attendees.ATTENDEE_STATUS_ACCEPTED));
834                                break;
835                            case Attendees.ATTENDEE_STATUS_DECLINED:
836                                mDeclinedAttendees.add(new Attendee(name, email,
837                                        Attendees.ATTENDEE_STATUS_DECLINED));
838                                break;
839                            case Attendees.ATTENDEE_STATUS_TENTATIVE:
840                                mTentativeAttendees.add(new Attendee(name, email,
841                                        Attendees.ATTENDEE_STATUS_TENTATIVE));
842                                break;
843                            default:
844                                mNoResponseAttendees.add(new Attendee(name, email,
845                                        Attendees.ATTENDEE_STATUS_NONE));
846                        }
847                    }
848                } while (mAttendeesCursor.moveToNext());
849                mAttendeesCursor.moveToFirst();
850
851                updateAttendees(view);
852            }
853        }
854    }
855
856    @Override
857    public void onSaveInstanceState(Bundle outState) {
858        super.onSaveInstanceState(outState);
859        outState.putLong(BUNDLE_KEY_EVENT_ID, mEventId);
860        outState.putLong(BUNDLE_KEY_START_MILLIS, mStartMillis);
861        outState.putLong(BUNDLE_KEY_END_MILLIS, mEndMillis);
862        outState.putBoolean(BUNDLE_KEY_IS_DIALOG, mIsDialog);
863        outState.putInt(BUNDLE_KEY_WINDOW_STYLE, mWindowStyle);
864        outState.putBoolean(BUNDLE_KEY_DELETE_DIALOG_VISIBLE, mDeleteDialogVisible);
865        outState.putInt(BUNDLE_KEY_ATTENDEE_RESPONSE, mAttendeeResponseFromIntent);
866    }
867
868
869    @Override
870    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
871        super.onCreateOptionsMenu(menu, inflater);
872        // Show edit/delete buttons only in non-dialog configuration
873        if (!mIsDialog && !mIsTabletConfig || mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) {
874            inflater.inflate(R.menu.event_info_title_bar, menu);
875            mMenu = menu;
876            updateMenu();
877        }
878    }
879
880    @Override
881    public boolean onOptionsItemSelected(MenuItem item) {
882
883        // If we're a dialog we don't want to handle menu buttons
884        if (mIsDialog) {
885            return false;
886        }
887        // Handles option menu selections:
888        // Home button - close event info activity and start the main calendar
889        // one
890        // Edit button - start the event edit activity and close the info
891        // activity
892        // Delete button - start a delete query that calls a runnable that close
893        // the info activity
894
895        switch (item.getItemId()) {
896            case android.R.id.home:
897                Utils.returnToCalendarHome(mContext);
898                mActivity.finish();
899                return true;
900            case R.id.info_action_edit:
901                Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
902                Intent intent = new Intent(Intent.ACTION_EDIT, uri);
903                intent.putExtra(EXTRA_EVENT_BEGIN_TIME, mStartMillis);
904                intent.putExtra(EXTRA_EVENT_END_TIME, mEndMillis);
905                intent.putExtra(EXTRA_EVENT_ALL_DAY, mAllDay);
906                intent.setClass(mActivity, EditEventActivity.class);
907                intent.putExtra(EVENT_EDIT_ON_LAUNCH, true);
908                startActivity(intent);
909                mActivity.finish();
910                break;
911            case R.id.info_action_delete:
912                mDeleteHelper =
913                        new DeleteEventHelper(mActivity, mActivity, true /* exitWhenDone */);
914                mDeleteHelper.setDeleteNotificationListener(EventInfoFragment.this);
915                mDeleteHelper.setOnDismissListener(createDeleteOnDismissListener());
916                mDeleteDialogVisible = true;
917                mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable);
918                break;
919            default:
920                break;
921        }
922        return super.onOptionsItemSelected(item);
923    }
924
925    @Override
926    public void onDestroyView() {
927
928        if (!mEventDeletionStarted) {
929            boolean responseSaved = saveResponse();
930            if (saveReminders() || responseSaved) {
931                Toast.makeText(getActivity(), R.string.saving_event, Toast.LENGTH_SHORT).show();
932            }
933        }
934        super.onDestroyView();
935    }
936
937    @Override
938    public void onDestroy() {
939        if (mEventCursor != null) {
940            mEventCursor.close();
941        }
942        if (mCalendarsCursor != null) {
943            mCalendarsCursor.close();
944        }
945        if (mAttendeesCursor != null) {
946            mAttendeesCursor.close();
947        }
948        super.onDestroy();
949    }
950
951    /**
952     * Asynchronously saves the response to an invitation if the user changed
953     * the response. Returns true if the database will be updated.
954     *
955     * @return true if the database will be changed
956     */
957    private boolean saveResponse() {
958        if (mAttendeesCursor == null || mEventCursor == null) {
959            return false;
960        }
961
962        RadioGroup radioGroup = (RadioGroup) getView().findViewById(R.id.response_value);
963        int status = getResponseFromButtonId(radioGroup.getCheckedRadioButtonId());
964        if (status == Attendees.ATTENDEE_STATUS_NONE) {
965            return false;
966        }
967
968        // If the status has not changed, then don't update the database
969        if (status == mOriginalAttendeeResponse) {
970            return false;
971        }
972
973        // If we never got an owner attendee id we can't set the status
974        if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE) {
975            return false;
976        }
977
978        if (!mIsRepeating) {
979            // This is a non-repeating event
980            updateResponse(mEventId, mCalendarOwnerAttendeeId, status);
981            return true;
982        }
983
984        // This is a repeating event
985        int whichEvents = mEditResponseHelper.getWhichEvents();
986        switch (whichEvents) {
987            case -1:
988                return false;
989            case UPDATE_SINGLE:
990                createExceptionResponse(mEventId, status);
991                return true;
992            case UPDATE_ALL:
993                updateResponse(mEventId, mCalendarOwnerAttendeeId, status);
994                return true;
995            default:
996                Log.e(TAG, "Unexpected choice for updating invitation response");
997                break;
998        }
999        return false;
1000    }
1001
1002    private void updateResponse(long eventId, long attendeeId, int status) {
1003        // Update the attendee status in the attendees table.  the provider
1004        // takes care of updating the self attendance status.
1005        ContentValues values = new ContentValues();
1006
1007        if (!TextUtils.isEmpty(mCalendarOwnerAccount)) {
1008            values.put(Attendees.ATTENDEE_EMAIL, mCalendarOwnerAccount);
1009        }
1010        values.put(Attendees.ATTENDEE_STATUS, status);
1011        values.put(Attendees.EVENT_ID, eventId);
1012
1013        Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, attendeeId);
1014
1015        mHandler.startUpdate(mHandler.getNextToken(), null, uri, values,
1016                null, null, Utils.UNDO_DELAY);
1017    }
1018
1019    /**
1020     * Creates an exception to a recurring event.  The only change we're making is to the
1021     * "self attendee status" value.  The provider will take care of updating the corresponding
1022     * Attendees.attendeeStatus entry.
1023     *
1024     * @param eventId The recurring event.
1025     * @param status The new value for selfAttendeeStatus.
1026     */
1027    private void createExceptionResponse(long eventId, int status) {
1028        ContentValues values = new ContentValues();
1029        values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
1030        values.put(Events.SELF_ATTENDEE_STATUS, status);
1031        values.put(Events.STATUS, Events.STATUS_CONFIRMED);
1032
1033        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
1034        Uri exceptionUri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI,
1035                String.valueOf(eventId));
1036        ops.add(ContentProviderOperation.newInsert(exceptionUri).withValues(values).build());
1037
1038        mHandler.startBatch(mHandler.getNextToken(), null, CalendarContract.AUTHORITY, ops,
1039                Utils.UNDO_DELAY);
1040   }
1041
1042    public static int getResponseFromButtonId(int buttonId) {
1043        int response;
1044        switch (buttonId) {
1045            case R.id.response_yes:
1046                response = Attendees.ATTENDEE_STATUS_ACCEPTED;
1047                break;
1048            case R.id.response_maybe:
1049                response = Attendees.ATTENDEE_STATUS_TENTATIVE;
1050                break;
1051            case R.id.response_no:
1052                response = Attendees.ATTENDEE_STATUS_DECLINED;
1053                break;
1054            default:
1055                response = Attendees.ATTENDEE_STATUS_NONE;
1056        }
1057        return response;
1058    }
1059
1060    public static int findButtonIdForResponse(int response) {
1061        int buttonId;
1062        switch (response) {
1063            case Attendees.ATTENDEE_STATUS_ACCEPTED:
1064                buttonId = R.id.response_yes;
1065                break;
1066            case Attendees.ATTENDEE_STATUS_TENTATIVE:
1067                buttonId = R.id.response_maybe;
1068                break;
1069            case Attendees.ATTENDEE_STATUS_DECLINED:
1070                buttonId = R.id.response_no;
1071                break;
1072                default:
1073                    buttonId = -1;
1074        }
1075        return buttonId;
1076    }
1077
1078    private void doEdit() {
1079        Context c = getActivity();
1080        // This ensures that we aren't in the process of closing and have been
1081        // unattached already
1082        if (c != null) {
1083            CalendarController.getInstance(c).sendEventRelatedEvent(
1084                    this, EventType.EDIT_EVENT, mEventId, mStartMillis, mEndMillis, 0
1085                    , 0, -1);
1086        }
1087    }
1088
1089    private void updateEvent(View view) {
1090        if (mEventCursor == null || view == null) {
1091            return;
1092        }
1093
1094        String eventName = mEventCursor.getString(EVENT_INDEX_TITLE);
1095        if (eventName == null || eventName.length() == 0) {
1096            eventName = getActivity().getString(R.string.no_title_label);
1097        }
1098
1099        mAllDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
1100        String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION);
1101        String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION);
1102        String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
1103        String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
1104
1105        mColor = Utils.getDisplayColorFromColor(mEventCursor.getInt(EVENT_INDEX_COLOR));
1106        mHeadlines.setBackgroundColor(mColor);
1107
1108        // What
1109        if (eventName != null) {
1110            setTextCommon(view, R.id.title, eventName);
1111        }
1112
1113        // When
1114        // Set the date and repeats (if any)
1115        String localTimezone = Utils.getTimeZone(mActivity, mTZUpdater);
1116        Activity context = getActivity();
1117        Resources resources = context.getResources();
1118        String displayedDatetime = Utils.getDisplayedDatetime(mStartMillis, mEndMillis,
1119                System.currentTimeMillis(), localTimezone, mAllDay, context);
1120        String displayedTimezone = Utils.getDisplayedTimezone(mStartMillis, localTimezone,
1121                eventTimezone);
1122        int timezoneIndex = displayedDatetime.length();
1123        if (displayedTimezone != null) {
1124            displayedDatetime += "  " + displayedTimezone;
1125        }
1126        // Display the datetime.  Make the timezone (if any) transparent.
1127        if (displayedTimezone == null) {
1128            setTextCommon(view, R.id.when_datetime, displayedDatetime);
1129        } else {
1130            SpannableStringBuilder sb = new SpannableStringBuilder(displayedDatetime);
1131            ForegroundColorSpan transparentColorSpan = new ForegroundColorSpan(
1132                    resources.getColor(R.color.event_info_headline_transparent_color));
1133            sb.setSpan(transparentColorSpan, timezoneIndex, displayedDatetime.length(),
1134                    Spannable.SPAN_INCLUSIVE_INCLUSIVE);
1135            setTextCommon(view, R.id.when_datetime, sb);
1136        }
1137
1138        // Display the repeat string (if any)
1139        String repeatString = null;
1140        if (!TextUtils.isEmpty(rRule)) {
1141            EventRecurrence eventRecurrence = new EventRecurrence();
1142            eventRecurrence.parse(rRule);
1143            Time date = new Time(localTimezone);
1144            date.set(mStartMillis);
1145            if (mAllDay) {
1146                date.timezone = Time.TIMEZONE_UTC;
1147            }
1148            eventRecurrence.setStartDate(date);
1149            repeatString = EventRecurrenceFormatter.getRepeatString(resources, eventRecurrence);
1150        }
1151        if (repeatString == null) {
1152            view.findViewById(R.id.when_repeat).setVisibility(View.GONE);
1153        } else {
1154            setTextCommon(view, R.id.when_repeat, repeatString);
1155        }
1156
1157        // Organizer view is setup in the updateCalendar method
1158
1159
1160        // Where
1161        if (location == null || location.trim().length() == 0) {
1162            setVisibilityCommon(view, R.id.where, View.GONE);
1163        } else {
1164            final TextView textView = mWhere;
1165            if (textView != null) {
1166                textView.setAutoLinkMask(0);
1167                textView.setText(location.trim());
1168                try {
1169                    linkifyTextView(textView);
1170                } catch (Exception ex) {
1171                    // unexpected
1172                    Log.e(TAG, "Linkification failed", ex);
1173                }
1174
1175                textView.setOnTouchListener(new OnTouchListener() {
1176                    @Override
1177                    public boolean onTouch(View v, MotionEvent event) {
1178                        try {
1179                            return v.onTouchEvent(event);
1180                        } catch (ActivityNotFoundException e) {
1181                            // ignore
1182                            return true;
1183                        }
1184                    }
1185                });
1186            }
1187        }
1188
1189        // Description
1190        if (description != null && description.length() != 0) {
1191            mDesc.setText(description);
1192        }
1193
1194    }
1195
1196    /**
1197     * Finds North American Numbering Plan (NANP) phone numbers in the input text.
1198     *
1199     * @param text The text to scan.
1200     * @return A list of [start, end) pairs indicating the positions of phone numbers in the input.
1201     */
1202    // @VisibleForTesting
1203    static int[] findNanpPhoneNumbers(CharSequence text) {
1204        ArrayList<Integer> list = new ArrayList<Integer>();
1205
1206        int startPos = 0;
1207        int endPos = text.length() - NANP_MIN_DIGITS + 1;
1208        if (endPos < 0) {
1209            return new int[] {};
1210        }
1211
1212        /*
1213         * We can't just strip the whitespace out and crunch it down, because the whitespace
1214         * is significant.  March through, trying to figure out where numbers start and end.
1215         */
1216        while (startPos < endPos) {
1217            // skip whitespace
1218            while (Character.isWhitespace(text.charAt(startPos)) && startPos < endPos) {
1219                startPos++;
1220            }
1221            if (startPos == endPos) {
1222                break;
1223            }
1224
1225            // check for a match at this position
1226            int matchEnd = findNanpMatchEnd(text, startPos);
1227            if (matchEnd > startPos) {
1228                list.add(startPos);
1229                list.add(matchEnd);
1230                startPos = matchEnd;    // skip past match
1231            } else {
1232                // skip to next whitespace char
1233                while (!Character.isWhitespace(text.charAt(startPos)) && startPos < endPos) {
1234                    startPos++;
1235                }
1236            }
1237        }
1238
1239        int[] result = new int[list.size()];
1240        for (int i = list.size() - 1; i >= 0; i--) {
1241            result[i] = list.get(i);
1242        }
1243        return result;
1244    }
1245
1246    /**
1247     * Checks to see if there is a valid phone number in the input, starting at the specified
1248     * offset.  If so, the index of the last character + 1 is returned.  The input is assumed
1249     * to begin with a non-whitespace character.
1250     *
1251     * @return Exclusive end position, or -1 if not a match.
1252     */
1253    private static int findNanpMatchEnd(CharSequence text, int startPos) {
1254        /*
1255         * A few interesting cases:
1256         *   94043                              # too short, ignore
1257         *   123456789012                       # too long, ignore
1258         *   +1 (650) 555-1212                  # 11 digits, spaces
1259         *   (650) 555-1212, (650) 555-1213     # two numbers, return first
1260         *   1-650-555-1212                     # 11 digits with leading '1'
1261         *   *#650.555.1212#*!                  # 10 digits, include #*, ignore trailing '!'
1262         *   555.1212                           # 7 digits
1263         *
1264         * For the most part we want to break on whitespace, but it's common to leave a space
1265         * between the initial '1' and/or after the area code.
1266         */
1267
1268        int endPos = text.length();
1269        int curPos = startPos;
1270        int foundDigits = 0;
1271        char firstDigit = 'x';
1272
1273        while (curPos <= endPos) {
1274            char ch;
1275            if (curPos < endPos) {
1276                ch = text.charAt(curPos);
1277            } else {
1278                ch = 27;    // fake invalid symbol at end to trigger loop break
1279            }
1280
1281            if (Character.isDigit(ch)) {
1282                if (foundDigits == 0) {
1283                    firstDigit = ch;
1284                }
1285                foundDigits++;
1286                if (foundDigits > NANP_MAX_DIGITS) {
1287                    // too many digits, stop early
1288                    return -1;
1289                }
1290            } else if (Character.isWhitespace(ch)) {
1291                if (!(  (firstDigit == '1' && (foundDigits == 1 || foundDigits == 4)) ||
1292                        (foundDigits == 3)) ) {
1293                    break;
1294                }
1295            } else if (NANP_ALLOWED_SYMBOLS.indexOf(ch) == -1) {
1296                break;
1297            }
1298            // else it's an allowed symbol
1299
1300            curPos++;
1301        }
1302
1303        if ((firstDigit != '1' && (foundDigits == 7 || foundDigits == 10)) ||
1304                (firstDigit == '1' && foundDigits == 11)) {
1305            // match
1306            return curPos;
1307        }
1308
1309        return -1;
1310    }
1311
1312    /**
1313     * Replaces stretches of text that look like addresses and phone numbers with clickable
1314     * links.
1315     * <p>
1316     * This is really just an enhanced version of Linkify.addLinks().
1317     */
1318    private static void linkifyTextView(TextView textView) {
1319        /*
1320         * If the text includes a street address like "1600 Amphitheater Parkway, 94043",
1321         * the current Linkify code will identify "94043" as a phone number and invite
1322         * you to dial it (and not provide a map link for the address).  We want to
1323         * have better recognition of phone numbers without losing any of the existing
1324         * annotations.
1325         *
1326         * Ideally this would be addressed by improving Linkify.  For now we manage it as
1327         * a second pass over the text.
1328         *
1329         * URIs and e-mail addresses are pretty easy to pick out of text.  Phone numbers
1330         * are a bit tricky because they have radically different formats in different
1331         * countries, in terms of both the digits and the way in which they are commonly
1332         * written or presented (e.g. the punctuation and spaces in "(650) 555-1212").
1333         * The expected format of a street address is defined in WebView.findAddress().  It's
1334         * pretty narrowly defined, so it won't often match.
1335         *
1336         * The RFC 3966 specification defines the format of a "tel:" URI.
1337         */
1338
1339        /*
1340         * If we're in the US, handle this specially.  Otherwise, punt to Linkify.
1341         */
1342        String defaultPhoneRegion = System.getProperty("user.region", "US");
1343        if (!defaultPhoneRegion.equals("US")) {
1344            Linkify.addLinks(textView, Linkify.ALL);
1345            return;
1346        }
1347
1348        /*
1349         * Start by letting Linkify find anything that isn't a phone number.  We have to let it
1350         * run first because every invocation removes all previous URLSpan annotations.
1351         *
1352         * Ideally we'd use the external/libphonenumber routines, but those aren't available
1353         * to unbundled applications.
1354         */
1355        boolean linkifyFoundLinks = Linkify.addLinks(textView,
1356                Linkify.ALL & ~(Linkify.PHONE_NUMBERS));
1357
1358        /*
1359         * Search for phone numbers.
1360         *
1361         * Some URIs contain strings of digits that look like phone numbers.  If both the URI
1362         * scanner and the phone number scanner find them, we want the URI link to win.  Since
1363         * the URI scanner runs first, we just need to avoid creating overlapping spans.
1364         */
1365        CharSequence text = textView.getText();
1366        int[] phoneSequences = findNanpPhoneNumbers(text);
1367
1368        /*
1369         * If the contents of the TextView are already Spannable (which will be the case if
1370         * Linkify found stuff, but might not be otherwise), we can just add annotations
1371         * to what's there.  If it's not, and we find phone numbers, we need to convert it to
1372         * a Spannable form.  (This mimics the behavior of Linkable.addLinks().)
1373         */
1374        Spannable spanText;
1375        if (text instanceof SpannableString) {
1376            spanText = (SpannableString) text;
1377        } else {
1378            spanText = SpannableString.valueOf(text);
1379        }
1380
1381        /*
1382         * Get a list of any spans created by Linkify, for the overlapping span check.
1383         */
1384        URLSpan[] existingSpans = spanText.getSpans(0, spanText.length(), URLSpan.class);
1385
1386        /*
1387         * Insert spans for the numbers we found.  We generate "tel:" URIs.
1388         */
1389        int phoneCount = 0;
1390        for (int match = 0; match < phoneSequences.length / 2; match++) {
1391            int start = phoneSequences[match*2];
1392            int end = phoneSequences[match*2 + 1];
1393
1394            if (spanWillOverlap(spanText, existingSpans, start, end)) {
1395                if (Log.isLoggable(TAG, Log.VERBOSE)) {
1396                    CharSequence seq = text.subSequence(start, end);
1397                    Log.v(TAG, "Not linkifying " + seq + " as phone number due to overlap");
1398                }
1399                continue;
1400            }
1401
1402            /*
1403             * The Linkify code takes the matching span and strips out everything that isn't a
1404             * digit or '+' sign.  We do the same here.  Extension numbers will get appended
1405             * without a separator, but the dialer wasn't doing anything useful with ";ext="
1406             * anyway.
1407             */
1408
1409            //String dialStr = phoneUtil.format(match.number(),
1410            //        PhoneNumberUtil.PhoneNumberFormat.RFC3966);
1411            StringBuilder dialBuilder = new StringBuilder();
1412            for (int i = start; i < end; i++) {
1413                char ch = spanText.charAt(i);
1414                if (ch == '+' || Character.isDigit(ch)) {
1415                    dialBuilder.append(ch);
1416                }
1417            }
1418            URLSpan span = new URLSpan("tel:" + dialBuilder.toString());
1419
1420            spanText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1421            phoneCount++;
1422        }
1423
1424        if (phoneCount != 0) {
1425            // If we had to "upgrade" to Spannable, store the object into the TextView.
1426            if (spanText != text) {
1427                textView.setText(spanText);
1428            }
1429
1430            // Linkify.addLinks() sets the TextView movement method if it finds any links.  We
1431            // want to do the same here.  (This is cloned from Linkify.addLinkMovementMethod().)
1432            MovementMethod mm = textView.getMovementMethod();
1433
1434            if ((mm == null) || !(mm instanceof LinkMovementMethod)) {
1435                if (textView.getLinksClickable()) {
1436                    textView.setMovementMethod(LinkMovementMethod.getInstance());
1437                }
1438            }
1439        }
1440
1441        if (!linkifyFoundLinks && phoneCount == 0) {
1442            if (Log.isLoggable(TAG, Log.VERBOSE)) {
1443                Log.v(TAG, "No linkification matches, using geo default");
1444            }
1445            Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q=");
1446        }
1447    }
1448
1449    /**
1450     * Determines whether a new span at [start,end) will overlap with any existing span.
1451     */
1452    private static boolean spanWillOverlap(Spannable spanText, URLSpan[] spanList, int start,
1453            int end) {
1454        if (start == end) {
1455            // empty span, ignore
1456            return false;
1457        }
1458        for (URLSpan span : spanList) {
1459            int existingStart = spanText.getSpanStart(span);
1460            int existingEnd = spanText.getSpanEnd(span);
1461            if ((start >= existingStart && start < existingEnd) ||
1462                    end > existingStart && end <= existingEnd) {
1463                return true;
1464            }
1465        }
1466
1467        return false;
1468    }
1469
1470    private void sendAccessibilityEvent() {
1471        AccessibilityManager am =
1472            (AccessibilityManager) getActivity().getSystemService(Service.ACCESSIBILITY_SERVICE);
1473        if (!am.isEnabled()) {
1474            return;
1475        }
1476
1477        AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
1478        event.setClassName(getClass().getName());
1479        event.setPackageName(getActivity().getPackageName());
1480        List<CharSequence> text = event.getText();
1481
1482        addFieldToAccessibilityEvent(text, mTitle, null);
1483        addFieldToAccessibilityEvent(text, mWhenDateTime, null);
1484        addFieldToAccessibilityEvent(text, mWhere, null);
1485        addFieldToAccessibilityEvent(text, null, mDesc);
1486
1487        RadioGroup response = (RadioGroup) getView().findViewById(R.id.response_value);
1488        if (response.getVisibility() == View.VISIBLE) {
1489            int id = response.getCheckedRadioButtonId();
1490            if (id != View.NO_ID) {
1491                text.add(((TextView) getView().findViewById(R.id.response_label)).getText());
1492                text.add((((RadioButton) (response.findViewById(id))).getText() + PERIOD_SPACE));
1493            }
1494        }
1495
1496        am.sendAccessibilityEvent(event);
1497    }
1498
1499    private void addFieldToAccessibilityEvent(List<CharSequence> text, TextView tv,
1500            ExpandableTextView etv) {
1501        CharSequence cs;
1502        if (tv != null) {
1503            cs = tv.getText();
1504        } else if (etv != null) {
1505            cs = etv.getText();
1506        } else {
1507            return;
1508        }
1509
1510        if (!TextUtils.isEmpty(cs)) {
1511            cs = cs.toString().trim();
1512            if (cs.length() > 0) {
1513                text.add(cs);
1514                text.add(PERIOD_SPACE);
1515            }
1516        }
1517    }
1518
1519    private void updateCalendar(View view) {
1520        mCalendarOwnerAccount = "";
1521        if (mCalendarsCursor != null && mEventCursor != null) {
1522            mCalendarsCursor.moveToFirst();
1523            String tempAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
1524            mCalendarOwnerAccount = (tempAccount == null) ? "" : tempAccount;
1525            mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0;
1526            mSyncAccountName = mCalendarsCursor.getString(CALENDARS_INDEX_ACCOUNT_NAME);
1527
1528            String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
1529
1530            // start duplicate calendars query
1531            mHandler.startQuery(TOKEN_QUERY_DUPLICATE_CALENDARS, null, Calendars.CONTENT_URI,
1532                    CALENDARS_PROJECTION, CALENDARS_DUPLICATE_NAME_WHERE,
1533                    new String[] {displayName}, null);
1534
1535            mEventOrganizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER);
1536            mIsOrganizer = mCalendarOwnerAccount.equalsIgnoreCase(mEventOrganizer);
1537            setTextCommon(view, R.id.organizer, mEventOrganizer);
1538            if (!mIsOrganizer) {
1539                setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE);
1540            } else {
1541                setVisibilityCommon(view, R.id.organizer_container, View.GONE);
1542            }
1543            mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0;
1544            mCanModifyCalendar = mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL)
1545                    >= Calendars.CAL_ACCESS_CONTRIBUTOR;
1546            // TODO add "|| guestCanModify" after b/1299071 is fixed
1547            mCanModifyEvent = mCanModifyCalendar && mIsOrganizer;
1548            mIsBusyFreeCalendar =
1549                    mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.CAL_ACCESS_FREEBUSY;
1550
1551            if (!mIsBusyFreeCalendar) {
1552                Button b = (Button) mView.findViewById(R.id.edit);
1553                b.setEnabled(true);
1554                b.setOnClickListener(new OnClickListener() {
1555                    @Override
1556                    public void onClick(View v) {
1557                        doEdit();
1558                        // For dialogs, just close the fragment
1559                        // For full screen, close activity on phone, leave it for tablet
1560                        if (mIsDialog) {
1561                            EventInfoFragment.this.dismiss();
1562                        }
1563                        else if (!mIsTabletConfig){
1564                            getActivity().finish();
1565                        }
1566                    }
1567                });
1568            }
1569            View button;
1570            if (mCanModifyCalendar) {
1571                button = mView.findViewById(R.id.delete);
1572                if (button != null) {
1573                    button.setEnabled(true);
1574                    button.setVisibility(View.VISIBLE);
1575                }
1576            }
1577            if (mCanModifyEvent) {
1578                button = mView.findViewById(R.id.edit);
1579                if (button != null) {
1580                    button.setEnabled(true);
1581                    button.setVisibility(View.VISIBLE);
1582                }
1583            }
1584
1585            if ((!mIsDialog && !mIsTabletConfig ||
1586                    mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) && mMenu != null) {
1587                mActivity.invalidateOptionsMenu();
1588            }
1589        } else {
1590            setVisibilityCommon(view, R.id.calendar, View.GONE);
1591            sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS);
1592        }
1593    }
1594
1595    /**
1596     *
1597     */
1598    private void updateMenu() {
1599        if (mMenu == null) {
1600            return;
1601        }
1602        MenuItem delete = mMenu.findItem(R.id.info_action_delete);
1603        MenuItem edit = mMenu.findItem(R.id.info_action_edit);
1604        if (delete != null) {
1605            delete.setVisible(mCanModifyCalendar);
1606            delete.setEnabled(mCanModifyCalendar);
1607        }
1608        if (edit != null) {
1609            edit.setVisible(mCanModifyEvent);
1610            edit.setEnabled(mCanModifyEvent);
1611        }
1612    }
1613
1614    private void updateAttendees(View view) {
1615        if (mAcceptedAttendees.size() + mDeclinedAttendees.size() +
1616                mTentativeAttendees.size() + mNoResponseAttendees.size() > 0) {
1617            mLongAttendees.clearAttendees();
1618            (mLongAttendees).addAttendees(mAcceptedAttendees);
1619            (mLongAttendees).addAttendees(mDeclinedAttendees);
1620            (mLongAttendees).addAttendees(mTentativeAttendees);
1621            (mLongAttendees).addAttendees(mNoResponseAttendees);
1622            mLongAttendees.setEnabled(false);
1623            mLongAttendees.setVisibility(View.VISIBLE);
1624        } else {
1625            mLongAttendees.setVisibility(View.GONE);
1626        }
1627
1628        updateEmailAttendees();
1629    }
1630
1631    /**
1632     * Initializes the list of 'to' and 'cc' emails from the attendee list.
1633     */
1634    private void updateEmailAttendees() {
1635        // The declined attendees will go in the 'cc' line, all others will go in the 'to' line.
1636        mToEmails = new ArrayList<String>();
1637        for (Attendee attendee : mAcceptedAttendees) {
1638            addIfEmailable(mToEmails, attendee.mEmail);
1639        }
1640        for (Attendee attendee : mTentativeAttendees) {
1641            addIfEmailable(mToEmails, attendee.mEmail);
1642        }
1643        for (Attendee attendee : mNoResponseAttendees) {
1644            addIfEmailable(mToEmails, attendee.mEmail);
1645        }
1646        mCcEmails = new ArrayList<String>();
1647        for (Attendee attendee : this.mDeclinedAttendees) {
1648            addIfEmailable(mCcEmails, attendee.mEmail);
1649        }
1650
1651        // The meeting organizer doesn't appear as an attendee sometimes (particularly
1652        // when viewing someone else's calendar), so add the organizer now.
1653        if (mEventOrganizer != null && !mToEmails.contains(mEventOrganizer) &&
1654                !mCcEmails.contains(mEventOrganizer)) {
1655            addIfEmailable(mToEmails, mEventOrganizer);
1656        }
1657
1658        // The Email app behaves strangely when there is nothing in the 'mailto' part,
1659        // so move all the 'cc' emails to the 'to' list.  Gmail works fine though.
1660        if (mToEmails.size() <= 0 && mCcEmails.size() > 0) {
1661            mToEmails.addAll(mCcEmails);
1662            mCcEmails.clear();
1663        }
1664
1665        if (mToEmails.size() <= 0) {
1666            setVisibilityCommon(mView, R.id.email_attendees_container, View.GONE);
1667        } else {
1668            setVisibilityCommon(mView, R.id.email_attendees_container, View.VISIBLE);
1669        }
1670    }
1671
1672    public void initReminders(View view, Cursor cursor) {
1673
1674        // Add reminders
1675        mOriginalReminders.clear();
1676        mUnsupportedReminders.clear();
1677        while (cursor.moveToNext()) {
1678            int minutes = cursor.getInt(EditEventHelper.REMINDERS_INDEX_MINUTES);
1679            int method = cursor.getInt(EditEventHelper.REMINDERS_INDEX_METHOD);
1680
1681            if (method != Reminders.METHOD_DEFAULT && !mReminderMethodValues.contains(method)) {
1682                // Stash unsupported reminder types separately so we don't alter
1683                // them in the UI
1684                mUnsupportedReminders.add(ReminderEntry.valueOf(minutes, method));
1685            } else {
1686                mOriginalReminders.add(ReminderEntry.valueOf(minutes, method));
1687            }
1688        }
1689        // Sort appropriately for display (by time, then type)
1690        Collections.sort(mOriginalReminders);
1691
1692        if (mUserModifiedReminders) {
1693            // If the user has changed the list of reminders don't change what's
1694            // shown.
1695            return;
1696        }
1697
1698        LinearLayout parent = (LinearLayout) mScrollView
1699                .findViewById(R.id.reminder_items_container);
1700        if (parent != null) {
1701            parent.removeAllViews();
1702        }
1703        if (mReminderViews != null) {
1704            mReminderViews.clear();
1705        }
1706
1707        if (mHasAlarm) {
1708            ArrayList<ReminderEntry> reminders = mOriginalReminders;
1709            // Insert any minute values that aren't represented in the minutes list.
1710            for (ReminderEntry re : reminders) {
1711                EventViewUtils.addMinutesToList(
1712                        mActivity, mReminderMinuteValues, mReminderMinuteLabels, re.getMinutes());
1713            }
1714            // Create a UI element for each reminder.  We display all of the reminders we get
1715            // from the provider, even if the count exceeds the calendar maximum.  (Also, for
1716            // a new event, we won't have a maxReminders value available.)
1717            for (ReminderEntry re : reminders) {
1718                EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
1719                        mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues,
1720                        mReminderMethodLabels, re, Integer.MAX_VALUE, mReminderChangeListener);
1721            }
1722            EventViewUtils.updateAddReminderButton(mView, mReminderViews, mMaxReminders);
1723            // TODO show unsupported reminder types in some fashion.
1724        }
1725    }
1726
1727    void updateResponse(View view) {
1728        // we only let the user accept/reject/etc. a meeting if:
1729        // a) you can edit the event's containing calendar AND
1730        // b) you're not the organizer and only attendee AND
1731        // c) organizerCanRespond is enabled for the calendar
1732        // (if the attendee data has been hidden, the visible number of attendees
1733        // will be 1 -- the calendar owner's).
1734        // (there are more cases involved to be 100% accurate, such as
1735        // paying attention to whether or not an attendee status was
1736        // included in the feed, but we're currently omitting those corner cases
1737        // for simplicity).
1738
1739        // TODO Switch to EditEventHelper.canRespond when this class uses CalendarEventModel.
1740        if (!mCanModifyCalendar || (mHasAttendeeData && mIsOrganizer && mNumOfAttendees <= 1) ||
1741                (mIsOrganizer && !mOwnerCanRespond)) {
1742            setVisibilityCommon(view, R.id.response_container, View.GONE);
1743            return;
1744        }
1745
1746        setVisibilityCommon(view, R.id.response_container, View.VISIBLE);
1747
1748
1749        int response;
1750        if (mUserSetResponse != CalendarController.ATTENDEE_NO_RESPONSE) {
1751            response = mUserSetResponse;
1752        } else if (mAttendeeResponseFromIntent != CalendarController.ATTENDEE_NO_RESPONSE) {
1753            response = mAttendeeResponseFromIntent;
1754        } else {
1755            response = mOriginalAttendeeResponse;
1756        }
1757
1758        int buttonToCheck = findButtonIdForResponse(response);
1759        RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.response_value);
1760        radioGroup.check(buttonToCheck); // -1 clear all radio buttons
1761        radioGroup.setOnCheckedChangeListener(this);
1762    }
1763
1764    private void setTextCommon(View view, int id, CharSequence text) {
1765        TextView textView = (TextView) view.findViewById(id);
1766        if (textView == null)
1767            return;
1768        textView.setText(text);
1769    }
1770
1771    private void setVisibilityCommon(View view, int id, int visibility) {
1772        View v = view.findViewById(id);
1773        if (v != null) {
1774            v.setVisibility(visibility);
1775        }
1776        return;
1777    }
1778
1779    /**
1780     * Taken from com.google.android.gm.HtmlConversationActivity
1781     *
1782     * Send the intent that shows the Contact info corresponding to the email address.
1783     */
1784    public void showContactInfo(Attendee attendee, Rect rect) {
1785        // First perform lookup query to find existing contact
1786        final ContentResolver resolver = getActivity().getContentResolver();
1787        final String address = attendee.mEmail;
1788        final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI,
1789                Uri.encode(address));
1790        final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri);
1791
1792        if (lookupUri != null) {
1793            // Found matching contact, trigger QuickContact
1794            QuickContact.showQuickContact(getActivity(), rect, lookupUri,
1795                    QuickContact.MODE_MEDIUM, null);
1796        } else {
1797            // No matching contact, ask user to create one
1798            final Uri mailUri = Uri.fromParts("mailto", address, null);
1799            final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, mailUri);
1800
1801            // Pass along full E-mail string for possible create dialog
1802            Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null);
1803            intent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION, sender.toString());
1804
1805            // Only provide personal name hint if we have one
1806            final String senderPersonal = attendee.mName;
1807            if (!TextUtils.isEmpty(senderPersonal)) {
1808                intent.putExtra(Intents.Insert.NAME, senderPersonal);
1809            }
1810
1811            startActivity(intent);
1812        }
1813    }
1814
1815    @Override
1816    public void onPause() {
1817        mIsPaused = true;
1818        mHandler.removeCallbacks(onDeleteRunnable);
1819        super.onPause();
1820        // Remove event deletion alert box since it is being rebuild in the OnResume
1821        // This is done to get the same behavior on OnResume since the AlertDialog is gone on
1822        // rotation but not if you press the HOME key
1823        if (mDeleteDialogVisible && mDeleteHelper != null) {
1824            mDeleteHelper.dismissAlertDialog();
1825            mDeleteHelper = null;
1826        }
1827    }
1828
1829    @Override
1830    public void onResume() {
1831        super.onResume();
1832        mIsPaused = false;
1833        if (mDismissOnResume) {
1834            mHandler.post(onDeleteRunnable);
1835        }
1836        // Display the "delete confirmation" dialog if needed
1837        if (mDeleteDialogVisible) {
1838            mDeleteHelper = new DeleteEventHelper(
1839                    mContext, mActivity,
1840                    !mIsDialog && !mIsTabletConfig /* exitWhenDone */);
1841            mDeleteHelper.setOnDismissListener(createDeleteOnDismissListener());
1842            mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable);
1843        }
1844    }
1845
1846    @Override
1847    public void eventsChanged() {
1848    }
1849
1850    @Override
1851    public long getSupportedEventTypes() {
1852        return EventType.EVENTS_CHANGED;
1853    }
1854
1855    @Override
1856    public void handleEvent(EventInfo event) {
1857        if (event.eventType == EventType.EVENTS_CHANGED && mHandler != null) {
1858            // reload the data
1859            reloadEvents();
1860        }
1861    }
1862
1863    public void reloadEvents() {
1864        mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION,
1865                null, null, null);
1866    }
1867
1868    @Override
1869    public void onClick(View view) {
1870
1871        // This must be a click on one of the "remove reminder" buttons
1872        LinearLayout reminderItem = (LinearLayout) view.getParent();
1873        LinearLayout parent = (LinearLayout) reminderItem.getParent();
1874        parent.removeView(reminderItem);
1875        mReminderViews.remove(reminderItem);
1876        mUserModifiedReminders = true;
1877        EventViewUtils.updateAddReminderButton(mView, mReminderViews, mMaxReminders);
1878    }
1879
1880
1881    /**
1882     * Add a new reminder when the user hits the "add reminder" button.  We use the default
1883     * reminder time and method.
1884     */
1885    private void addReminder() {
1886        // TODO: when adding a new reminder, make it different from the
1887        // last one in the list (if any).
1888        if (mDefaultReminderMinutes == GeneralPreferences.NO_REMINDER) {
1889            EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
1890                    mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues,
1891                    mReminderMethodLabels,
1892                    ReminderEntry.valueOf(GeneralPreferences.REMINDER_DEFAULT_TIME), mMaxReminders,
1893                    mReminderChangeListener);
1894        } else {
1895            EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
1896                    mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues,
1897                    mReminderMethodLabels, ReminderEntry.valueOf(mDefaultReminderMinutes),
1898                    mMaxReminders, mReminderChangeListener);
1899        }
1900
1901        EventViewUtils.updateAddReminderButton(mView, mReminderViews, mMaxReminders);
1902    }
1903
1904    synchronized private void prepareReminders() {
1905        // Nothing to do if we've already built these lists _and_ we aren't
1906        // removing not allowed methods
1907        if (mReminderMinuteValues != null && mReminderMinuteLabels != null
1908                && mReminderMethodValues != null && mReminderMethodLabels != null
1909                && mCalendarAllowedReminders == null) {
1910            return;
1911        }
1912        // Load the labels and corresponding numeric values for the minutes and methods lists
1913        // from the assets.  If we're switching calendars, we need to clear and re-populate the
1914        // lists (which may have elements added and removed based on calendar properties).  This
1915        // is mostly relevant for "methods", since we shouldn't have any "minutes" values in a
1916        // new event that aren't in the default set.
1917        Resources r = mActivity.getResources();
1918        mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values);
1919        mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels);
1920        mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values);
1921        mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels);
1922
1923        // Remove any reminder methods that aren't allowed for this calendar.  If this is
1924        // a new event, mCalendarAllowedReminders may not be set the first time we're called.
1925        if (mCalendarAllowedReminders != null) {
1926            EventViewUtils.reduceMethodList(mReminderMethodValues, mReminderMethodLabels,
1927                    mCalendarAllowedReminders);
1928        }
1929        if (mView != null) {
1930            mView.invalidate();
1931        }
1932    }
1933
1934
1935    private boolean saveReminders() {
1936        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(3);
1937
1938        // Read reminders from UI
1939        mReminders = EventViewUtils.reminderItemsToReminders(mReminderViews,
1940                mReminderMinuteValues, mReminderMethodValues);
1941        mOriginalReminders.addAll(mUnsupportedReminders);
1942        Collections.sort(mOriginalReminders);
1943        mReminders.addAll(mUnsupportedReminders);
1944        Collections.sort(mReminders);
1945
1946        // Check if there are any changes in the reminder
1947        boolean changed = EditEventHelper.saveReminders(ops, mEventId, mReminders,
1948                mOriginalReminders, false /* no force save */);
1949
1950        if (!changed) {
1951            return false;
1952        }
1953
1954        // save new reminders
1955        AsyncQueryService service = new AsyncQueryService(getActivity());
1956        service.startBatch(0, null, Calendars.CONTENT_URI.getAuthority(), ops, 0);
1957        // Update the "hasAlarm" field for the event
1958        Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
1959        int len = mReminders.size();
1960        boolean hasAlarm = len > 0;
1961        if (hasAlarm != mHasAlarm) {
1962            ContentValues values = new ContentValues();
1963            values.put(Events.HAS_ALARM, hasAlarm ? 1 : 0);
1964            service.startUpdate(0, null, uri, values, null, null, 0);
1965        }
1966        return true;
1967    }
1968
1969    /**
1970     * Adds the attendee's email to the list if:
1971     *   (1) the attendee is not a resource like a conference room or another calendar.
1972     *       Catch most of these by filtering out suffix calendar.google.com.
1973     *   (2) the attendee is not the viewer, to prevent mailing himself.
1974     */
1975    private void addIfEmailable(ArrayList<String> emailList, String email) {
1976        if (email != null && !email.equals(mSyncAccountName) &&
1977                !email.endsWith("calendar.google.com")) {
1978            emailList.add(email);
1979        }
1980    }
1981
1982    /**
1983     * Email all the attendees of the event, except for the viewer (so as to not email
1984     * himself) and resources like conference rooms.
1985     */
1986    private void emailAttendees() {
1987        String eventTitle = (mTitle == null || mTitle.getText() == null) ? null :
1988                mTitle.getText().toString();
1989        Intent emailIntent = Utils.createEmailAttendeesIntent(getActivity().getResources(),
1990                eventTitle, mToEmails, mCcEmails, mCalendarOwnerAccount);
1991        startActivity(emailIntent);
1992    }
1993
1994    /**
1995     * Loads an integer array asset into a list.
1996     */
1997    private static ArrayList<Integer> loadIntegerArray(Resources r, int resNum) {
1998        int[] vals = r.getIntArray(resNum);
1999        int size = vals.length;
2000        ArrayList<Integer> list = new ArrayList<Integer>(size);
2001
2002        for (int i = 0; i < size; i++) {
2003            list.add(vals[i]);
2004        }
2005
2006        return list;
2007    }
2008    /**
2009     * Loads a String array asset into a list.
2010     */
2011    private static ArrayList<String> loadStringArray(Resources r, int resNum) {
2012        String[] labels = r.getStringArray(resNum);
2013        ArrayList<String> list = new ArrayList<String>(Arrays.asList(labels));
2014        return list;
2015    }
2016
2017    public void onDeleteStarted() {
2018        mEventDeletionStarted = true;
2019    }
2020
2021    private Dialog.OnDismissListener createDeleteOnDismissListener() {
2022        return new Dialog.OnDismissListener() {
2023                    @Override
2024                    public void onDismiss(DialogInterface dialog) {
2025                        // Since OnPause will force the dialog to dismiss , do
2026                        // not change the dialog status
2027                        if (!mIsPaused) {
2028                            mDeleteDialogVisible = false;
2029                        }
2030                    }
2031                };
2032    }
2033
2034    public long getEventId() {
2035        return mEventId;
2036    }
2037
2038    public long getStartMillis() {
2039        return mStartMillis;
2040    }
2041    public long getEndMillis() {
2042        return mEndMillis;
2043    }
2044
2045
2046
2047}
2048