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