diff options
author | Rohit Yengisetty <rohit@cyngn.com> | 2015-01-20 16:56:13 -0800 |
---|---|---|
committer | Rohit Yengisetty <rohit@cyngn.com> | 2015-01-21 16:27:39 -0800 |
commit | 836ee449b9db7c08347e66308a978d9535c47833 (patch) | |
tree | bb2a5c252ea2fc5323cdae79fa8aa08b94f23f7d | |
parent | b044a3adf0dbfa496194f098152d37b9299add7b (diff) | |
download | android_packages_apps_Calendar-836ee449b9db7c08347e66308a978d9535c47833.tar.gz android_packages_apps_Calendar-836ee449b9db7c08347e66308a978d9535c47833.tar.bz2 android_packages_apps_Calendar-836ee449b9db7c08347e66308a978d9535c47833.zip |
Calendar : Respond to calendar events share intent
Retrofit existing classes to support launching Calendar in
"shareable" mode. User will have the ability to select calendar
events for whom vcs files will be generated and sent to
the requester.
Change-Id: I2b0162ea53a4392149aa49765b8b070db36daa8a
-rw-r--r-- | AndroidManifest.xml | 10 | ||||
-rw-r--r-- | res/layout/agenda_item.xml | 19 | ||||
-rw-r--r-- | res/menu/share_event_title_bar.xml | 29 | ||||
-rw-r--r-- | res/values/cm_plurals.xml | 11 | ||||
-rw-r--r-- | src/com/android/calendar/CalendarUtils.java | 77 | ||||
-rw-r--r-- | src/com/android/calendar/EventInfoFragment.java | 251 | ||||
-rw-r--r-- | src/com/android/calendar/ShareCalendarActivity.java | 229 | ||||
-rw-r--r-- | src/com/android/calendar/agenda/AgendaAdapter.java | 3 | ||||
-rw-r--r-- | src/com/android/calendar/agenda/AgendaFragment.java | 33 | ||||
-rw-r--r-- | src/com/android/calendar/agenda/AgendaListView.java | 43 | ||||
-rw-r--r-- | src/com/android/calendar/agenda/AgendaWindowAdapter.java | 60 | ||||
-rw-r--r-- | src/com/android/calendar/icalendar/VCalendar.java | 21 |
12 files changed, 668 insertions, 118 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index bb4bf3e3..bc36311d 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -178,6 +178,16 @@ android:value="com.android.calendar.AllInOneActivity" /> </activity> + <activity android:name=".ShareCalendarActivity" + android:theme="@style/CalendarTheme.WithActionBar"> + <intent-filter> + <action android:name="android.intent.action.GET_CONTENT"/> + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.APP_CALENDAR" /> + <data android:mimeType="text/x-vCalendar"/> + </intent-filter> + </activity> + <provider android:name=".CalendarRecentSuggestionsProvider" android:exported="false" android:authorities="com.android.calendar.CalendarRecentSuggestionsProvider" /> diff --git a/res/layout/agenda_item.xml b/res/layout/agenda_item.xml index 16f81506..8d90581f 100644 --- a/res/layout/agenda_item.xml +++ b/res/layout/agenda_item.xml @@ -20,7 +20,7 @@ android:layout_height="wrap_content" android:layout_width="match_parent" android:minHeight="64dip" - android:columnCount="3" + android:columnCount="4" android:rowCount="2"> <View android:layout_height="1px" @@ -28,7 +28,7 @@ android:layout_column="0" android:layout_row="0" android:layout_rowSpan="1" - android:layout_columnSpan="3" + android:layout_columnSpan="4" android:layout_width="match_parent" /> <com.android.calendar.ColorChipView android:id="@+id/agenda_item_color" @@ -86,9 +86,22 @@ android:textColor="@color/agenda_item_where_text_color" style="?android:attr/textAppearanceSmallInverse" /> </LinearLayout> + + <CheckBox + android:layout_width="24dp" + android:layout_height="24dp" + android:id="@+id/shareCheckbox" + android:layout_row="1" + android:layout_column="2" + android:layout_rowSpan="1" + android:layout_gravity="center_vertical" + android:layout_marginRight="10dip" + android:focusable="false" + android:visibility="gone"/> + <ImageView android:id="@+id/selected_marker" - android:layout_column="2" + android:layout_column="3" android:layout_row="1" android:layout_rowSpan="1" android:layout_width="wrap_content" diff --git a/res/menu/share_event_title_bar.xml b/res/menu/share_event_title_bar.xml new file mode 100644 index 00000000..3f8b326c --- /dev/null +++ b/res/menu/share_event_title_bar.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<menu + xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/action_cancel" + android:alphabeticShortcut="c" + android:title="@string/discard_label" + android:icon="@drawable/ic_menu_cancel_holo_light" + android:showAsAction="withText|always" /> + <item android:id="@+id/action_done" + android:alphabeticShortcut="d" + android:title="@string/save_label" + android:icon="@drawable/ic_menu_done_holo_light" + android:showAsAction="withText|always" /> +</menu> diff --git a/res/values/cm_plurals.xml b/res/values/cm_plurals.xml index d7caea6f..48fae5ab 100644 --- a/res/values/cm_plurals.xml +++ b/res/values/cm_plurals.xml @@ -15,7 +15,16 @@ limitations under the License. --> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <plurals name="events_selected"> - <item quantity="other">%1$d selected</item> + <item quantity="zero">No events selected</item> + <item quantity="one">%1$d event selected</item> + <item quantity="other">%1$d events selected</item> + </plurals> + + <plurals name="select_events_to_share"> + <item quantity="one">Select an event to share</item> + <item quantity="other">Select events to share</item> </plurals> + </resources> diff --git a/src/com/android/calendar/CalendarUtils.java b/src/com/android/calendar/CalendarUtils.java index 0238c321..7b9ffc10 100644 --- a/src/com/android/calendar/CalendarUtils.java +++ b/src/com/android/calendar/CalendarUtils.java @@ -103,7 +103,7 @@ public class CalendarUtils { // Check the values in the db int keyColumn = cursor.getColumnIndexOrThrow(CalendarCache.KEY); int valueColumn = cursor.getColumnIndexOrThrow(CalendarCache.VALUE); - while(cursor.moveToNext()) { + while (cursor.moveToNext()) { String key = cursor.getString(keyColumn); String value = cursor.getString(valueColumn); if (TextUtils.equals(key, CalendarCache.KEY_TIMEZONE_TYPE)) { @@ -353,4 +353,79 @@ public class CalendarUtils { public static SharedPreferences getSharedPreferences(Context context, String prefsName) { return context.getSharedPreferences(prefsName, Context.MODE_PRIVATE); } + + /** + * For those interested in changes to the list of shared calendar events + */ + public static interface ShareEventListener { + + /** + * Called whenever a calendar event is added to the list of events about to be shared + * + * @param eventInfo A Triple containing event related information + * Triple (event id, startMillis, endMillis) + */ + public void onEventShared(Triple<Long, Long, Long> eventInfo); + + + /** + * called when an event is removed from the share list + */ + public void onEventRemoval(long eventId); + + /** + * called to signal that the current share list has been discarded + */ + public void onResetShareList(); + } + + /** + * Implementation of a 3-tuple + * Modeled after {@link android.util.Pair} for passing calendar event information + * + * The first element is perceived to be the unique identifier with the others serving as storage + * for ancillary information + */ + public static class Triple<X, Y, Z> { + public X first; // dominating element used in equals() and hashCode() implementations + public Y second; + public Z third; + + public Triple(X a, Y b, Z c) { + first = a; + second = b; + third = c; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("["); + builder.append(first.toString()); + builder.append(" , "); + builder.append(second.toString()); + builder.append(" , "); + builder.append(third.toString()); + builder.append("]"); + return builder.toString(); + } + + /** + * First element in the triple is used for comparison, as the other are used to store + * ancillary information + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof Triple)) return false; + return ((Triple)o).first.equals(first); + } + + /** + * Only the first element contributes + */ + @Override + public int hashCode() { + return first.hashCode(); + } + } } diff --git a/src/com/android/calendar/EventInfoFragment.java b/src/com/android/calendar/EventInfoFragment.java index 4374226e..dd4d17a6 100644 --- a/src/com/android/calendar/EventInfoFragment.java +++ b/src/com/android/calendar/EventInfoFragment.java @@ -427,6 +427,10 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange private QueryHandler mHandler; + // used to signal the completion of querying calendar event data + // note: runnable is executed on the ui thread + private Runnable mQueryCompleteRunnable; + private final Runnable mTZUpdater = new Runnable() { @Override public void run() { @@ -463,6 +467,8 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange private CalendarController mController; + private boolean mInNonUiMode; + private class QueryHandler extends AsyncQueryService { public QueryHandler(Context context) { super(context); @@ -471,7 +477,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { // if the activity is finishing, then close the cursor and return - final Activity activity = getActivity(); + final Activity activity = (mActivity != null) ? mActivity : getActivity(); if (activity == null || activity.isFinishing()) { if (cursor != null) { cursor.close(); @@ -517,9 +523,10 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange case TOKEN_QUERY_CALENDARS: mCalendarsCursor = Utils.matrixCursorFromCursor(cursor); updateCalendar(mView); - // FRAG_TODO fragments shouldn't set the title anymore - updateTitle(); - + if (!mInNonUiMode) { + // FRAG_TODO fragments shouldn't set the title anymore + updateTitle(); + } args = new String[] { mCalendarsCursor.getString(CALENDARS_INDEX_ACCOUNT_NAME), mCalendarsCursor.getString(CALENDARS_INDEX_ACCOUNT_TYPE) }; @@ -535,7 +542,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange startQuery(TOKEN_QUERY_ATTENDEES, null, uri, ATTENDEES_PROJECTION, ATTENDEES_WHERE, args, ATTENDEES_SORT_ORDER); } else { - sendAccessibilityEventIfQueryDone(TOKEN_QUERY_ATTENDEES); + assessQueryCompletion(TOKEN_QUERY_ATTENDEES); } if (mHasAlarm) { // start reminders query @@ -544,10 +551,11 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange startQuery(TOKEN_QUERY_REMINDERS, null, uri, REMINDERS_PROJECTION, REMINDERS_WHERE, args, null); } else { - sendAccessibilityEventIfQueryDone(TOKEN_QUERY_REMINDERS); + assessQueryCompletion(TOKEN_QUERY_REMINDERS); } break; case TOKEN_QUERY_COLORS: + if (mInNonUiMode) break; ArrayList<Integer> colors = new ArrayList<Integer>(); if (cursor.moveToFirst()) { do @@ -584,11 +592,11 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange case TOKEN_QUERY_ATTENDEES: mAttendeesCursor = Utils.matrixCursorFromCursor(cursor); initAttendeesCursor(mView); - updateResponse(mView); + if (!mInNonUiMode) updateResponse(mView); break; case TOKEN_QUERY_REMINDERS: mRemindersCursor = Utils.matrixCursorFromCursor(cursor); - initReminders(mView, mRemindersCursor); + if (!mInNonUiMode) initReminders(mView, mRemindersCursor); break; case TOKEN_QUERY_VISIBLE_CALENDARS: if (cursor.getCount() > 1) { @@ -601,34 +609,38 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } else { // Don't need to display the calendar owner when there is only a single // calendar. Skip the duplicate calendars query. - setVisibilityCommon(mView, R.id.calendar_container, View.GONE); + if (!mInNonUiMode) { + setVisibilityCommon(mView, R.id.calendar_container, View.GONE); + } mCurrentQuery |= TOKEN_QUERY_DUPLICATE_CALENDARS; } break; case TOKEN_QUERY_DUPLICATE_CALENDARS: - SpannableStringBuilder sb = new SpannableStringBuilder(); - - // Calendar display name - String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); - sb.append(calendarName); - - // Show email account if display name is not unique and - // display name != email - String email = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); - if (cursor.getCount() > 1 && !calendarName.equalsIgnoreCase(email) && - Utils.isValidEmail(email)) { - sb.append(" (").append(email).append(")"); - } + if (!mInNonUiMode) { + SpannableStringBuilder sb = new SpannableStringBuilder(); + + // Calendar display name + String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); + sb.append(calendarName); + + // Show email account if display name is not unique and + // display name != email + String email = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); + if (cursor.getCount() > 1 && !calendarName.equalsIgnoreCase(email) && + Utils.isValidEmail(email)) { + sb.append(" (").append(email).append(")"); + } - setVisibilityCommon(mView, R.id.calendar_container, View.VISIBLE); - setTextCommon(mView, R.id.calendar_name, sb); + setVisibilityCommon(mView, R.id.calendar_container, View.VISIBLE); + setTextCommon(mView, R.id.calendar_name, sb); + } break; } cursor.close(); - sendAccessibilityEventIfQueryDone(token); + assessQueryCompletion(token); // All queries are done, show the view. - if (mCurrentQuery == TOKEN_QUERY_ALL) { + if (mCurrentQuery == TOKEN_QUERY_ALL && !mInNonUiMode) { if (mLoadingMsgView.getAlpha() == 1) { // Loading message is showing, let it stay a bit more (to prevent // flashing) by adding a start delay to the event animation @@ -648,10 +660,18 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } } - private void sendAccessibilityEventIfQueryDone(int token) { + private void assessQueryCompletion(int token) { mCurrentQuery |= token; if (mCurrentQuery == TOKEN_QUERY_ALL) { - sendAccessibilityEvent(); + + // signal query completion + if (mQueryCompleteRunnable != null) { + mActivity.runOnUiThread(mQueryCompleteRunnable); + } + if (!mInNonUiMode) { + sendAccessibilityEvent(); + } + } } @@ -699,6 +719,10 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange mEventId = eventId; } + public void launchInNonUiMode() { + mInNonUiMode = true; + } + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -739,6 +763,10 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } } + public void setQueryCompleteRunnable(Runnable runnable) { + mQueryCompleteRunnable = runnable; + } + private void applyDialogParams() { Dialog dialog = getDialog(); dialog.setCanceledOnTouchOutside(true); @@ -870,6 +898,21 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } } + /** + * Initiate the querying of the calendar event data. + * For when this component is started in a non-ui mode + */ + public void startQueryingData(Context context) { + mContext = context; + if (context instanceof Activity) { + mActivity = (Activity) context; + } + + mHandler = new QueryHandler(context); + mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION, + null, null, null); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -1112,7 +1155,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange // Overwrites the one from Event table if available if (!TextUtils.isEmpty(name)) { mEventOrganizerDisplayName = name; - if (!mIsOrganizer) { + if (!mIsOrganizer && view != null) { setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE); setTextCommon(view, R.id.organizer, mEventOrganizerDisplayName); } @@ -1160,7 +1203,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } while (mAttendeesCursor.moveToNext()); mAttendeesCursor.moveToFirst(); - updateAttendees(view); + if (view != null) updateAttendees(view); } } } @@ -1278,64 +1321,13 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange * Generates an .ics formatted file with the event info and launches intent chooser to * share said file */ - private void shareEvent(ShareType type) { - // Create the respective ICalendar objects from the event info - VCalendar calendar = new VCalendar(); - calendar.addProperty(VCalendar.VERSION, "2.0"); - calendar.addProperty(VCalendar.PRODID, VCalendar.PRODUCT_IDENTIFIER); - calendar.addProperty(VCalendar.CALSCALE, "GREGORIAN"); - calendar.addProperty(VCalendar.METHOD, "REQUEST"); - - VEvent event = new VEvent(); - mEventCursor.moveToFirst(); - // add event start and end datetime - if (!mAllDay) { - String eventTimeZone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE); - event.addEventStart(mStartMillis, eventTimeZone); - event.addEventEnd(mEndMillis, eventTimeZone); - } else { - // All-day events' start and end time are stored as UTC. - // Treat the event start and end time as being in the local time zone and convert them - // to the corresponding UTC datetime. If the UTC time is used as is, the ical recipients - // will report the wrong start and end time (+/- 1 day) for the event as they will - // convert the UTC time to their respective local time-zones - String localTimeZone = Utils.getTimeZone(mActivity, mTZUpdater); - long eventStart = IcalendarUtils.convertTimeToUtc(mStartMillis, localTimeZone); - long eventEnd = IcalendarUtils.convertTimeToUtc(mEndMillis, localTimeZone); - event.addEventStart(eventStart, "UTC"); - event.addEventEnd(eventEnd, "UTC"); - } - - event.addProperty(VEvent.LOCATION, mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION)); - event.addProperty(VEvent.DESCRIPTION, mEventCursor.getString(EVENT_INDEX_DESCRIPTION)); - event.addProperty(VEvent.SUMMARY, mEventCursor.getString(EVENT_INDEX_TITLE)); - event.addOrganizer(new Organizer(mEventOrganizerDisplayName, mEventOrganizerEmail)); - - // Add Attendees to event - for (Attendee attendee : mAcceptedAttendees) { - IcalendarUtils.addAttendeeToEvent(attendee, event); - } - - for (Attendee attendee : mDeclinedAttendees) { - IcalendarUtils.addAttendeeToEvent(attendee, event); - } - - for (Attendee attendee : mTentativeAttendees) { - IcalendarUtils.addAttendeeToEvent(attendee, event); - } - - for (Attendee attendee : mNoResponseAttendees) { - IcalendarUtils.addAttendeeToEvent(attendee, event); - } - - // compose all of the ICalendar objects - calendar.addEvent(event); - + public void shareEvent(ShareType type) { + VCalendar calendar = generateVCalendar(); // create and share ics file boolean isShareSuccessful = false; try { // event title serves as the file name prefix - String filePrefix = event.getProperty(VEvent.SUMMARY); + String filePrefix = calendar.getFirstEvent().getProperty(VEvent.SUMMARY); if (filePrefix == null || filePrefix.length() < 3) { // default to a generic filename if event title doesn't qualify // prefix length constraint is imposed by File#createTempFile @@ -1358,8 +1350,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange dir = mActivity.getExternalCacheDir(); } - File inviteFile = IcalendarUtils.createTempFile(filePrefix, ".ics", - dir); + File inviteFile = IcalendarUtils.createTempFile(filePrefix, ".ics", dir); if (IcalendarUtils.writeCalendarToFile(calendar, inviteFile)) { if (type == ShareType.INTENT) { @@ -1393,9 +1384,11 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } startActivity(chooserIntent); } else { - String msg = getString(R.string.cal_export_succ_msg); - Toast.makeText(mActivity, String.format(msg, inviteFile), - Toast.LENGTH_SHORT).show(); + if (! mInNonUiMode) { + String msg = getString(R.string.cal_export_succ_msg); + Toast.makeText(mActivity, String.format(msg, inviteFile), + Toast.LENGTH_SHORT).show(); + } } isShareSuccessful = true; @@ -1414,6 +1407,66 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } } + /** + * Creates a calendar object (VCalendar) that adheres to the ICalendar specification + */ + public VCalendar generateVCalendar() { + + // Create the respective ICalendar objects from the event info + VCalendar calendar = new VCalendar(); + calendar.addProperty(VCalendar.VERSION, "2.0"); + calendar.addProperty(VCalendar.PRODID, VCalendar.PRODUCT_IDENTIFIER); + calendar.addProperty(VCalendar.CALSCALE, "GREGORIAN"); + calendar.addProperty(VCalendar.METHOD, "REQUEST"); + + VEvent event = new VEvent(); + mEventCursor.moveToFirst(); + // add event start and end datetime + if (!mAllDay) { + String eventTimeZone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE); + event.addEventStart(mStartMillis, eventTimeZone); + event.addEventEnd(mEndMillis, eventTimeZone); + } else { + // All-day events' start and end time are stored as UTC. + // Treat the event start and end time as being in the local time zone and convert them + // to the corresponding UTC datetime. If the UTC time is used as is, the ical recipients + // will report the wrong start and end time (+/- 1 day) for the event as they will + // convert the UTC time to their respective local time-zones + String localTimeZone = Utils.getTimeZone(mActivity, mTZUpdater); + long eventStart = IcalendarUtils.convertTimeToUtc(mStartMillis, localTimeZone); + long eventEnd = IcalendarUtils.convertTimeToUtc(mEndMillis, localTimeZone); + event.addEventStart(eventStart, "UTC"); + event.addEventEnd(eventEnd, "UTC"); + } + + event.addProperty(VEvent.LOCATION, mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION)); + event.addProperty(VEvent.DESCRIPTION, mEventCursor.getString(EVENT_INDEX_DESCRIPTION)); + event.addProperty(VEvent.SUMMARY, mEventCursor.getString(EVENT_INDEX_TITLE)); + event.addOrganizer(new Organizer(mEventOrganizerDisplayName, mEventOrganizerEmail)); + + // Add Attendees to event + for (Attendee attendee : mAcceptedAttendees) { + IcalendarUtils.addAttendeeToEvent(attendee, event); + } + + for (Attendee attendee : mDeclinedAttendees) { + IcalendarUtils.addAttendeeToEvent(attendee, event); + } + + for (Attendee attendee : mTentativeAttendees) { + IcalendarUtils.addAttendeeToEvent(attendee, event); + } + + for (Attendee attendee : mNoResponseAttendees) { + IcalendarUtils.addAttendeeToEvent(attendee, event); + } + + // compose all of the ICalendar objects + calendar.addEvent(event); + + return calendar; + } + private void showEventColorPickerDialog() { if (mColorPickerDialog == null) { mColorPickerDialog = EventColorPickerDialog.newInstance(mColors, mCurrentColor, @@ -1925,12 +1978,15 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange mEventOrganizerDisplayName = mEventOrganizerEmail; } - if (!mIsOrganizer && !TextUtils.isEmpty(mEventOrganizerDisplayName)) { - setTextCommon(view, R.id.organizer, mEventOrganizerDisplayName); - setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE); - } else { - setVisibilityCommon(view, R.id.organizer_container, View.GONE); + if (!mInNonUiMode) { + if (!mIsOrganizer && !TextUtils.isEmpty(mEventOrganizerDisplayName)) { + setTextCommon(view, R.id.organizer, mEventOrganizerDisplayName); + setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE); + } else { + setVisibilityCommon(view, R.id.organizer_container, View.GONE); + } } + mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0; mCanModifyCalendar = mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) >= Calendars.CAL_ACCESS_CONTRIBUTOR; @@ -1939,6 +1995,9 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange mIsBusyFreeCalendar = mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.CAL_ACCESS_FREEBUSY; + // no more work to be done if launched in non-ui mode + if (mInNonUiMode) return; + if (!mIsBusyFreeCalendar) { View b = mView.findViewById(R.id.edit); @@ -1978,8 +2037,8 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange mActivity.invalidateOptionsMenu(); } } else { - setVisibilityCommon(view, R.id.calendar, View.GONE); - sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS); + if (!mInNonUiMode) setVisibilityCommon(view, R.id.calendar, View.GONE); + assessQueryCompletion(TOKEN_QUERY_DUPLICATE_CALENDARS); } } diff --git a/src/com/android/calendar/ShareCalendarActivity.java b/src/com/android/calendar/ShareCalendarActivity.java new file mode 100644 index 00000000..cd538ca6 --- /dev/null +++ b/src/com/android/calendar/ShareCalendarActivity.java @@ -0,0 +1,229 @@ +package com.android.calendar; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.Bundle; +import android.provider.CalendarContract; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import com.android.calendar.agenda.AgendaFragment; +import com.android.calendar.icalendar.IcalendarUtils; +import com.android.calendar.icalendar.VCalendar; +import com.android.calendar.icalendar.VEvent; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; + +/** + * Handles requests for sharing calendar events + * This activity returns a vcs formatted file + */ +public class ShareCalendarActivity extends Activity implements CalendarUtils.ShareEventListener { + + public static final String EXTRA_LIMIT_TO_ONE_EVENT = "EXTRA_LIMIT_TO_ONE_EVENT"; + + private static final String TAG = "ShareCalendarActivity"; + + private HashSet<CalendarUtils.Triple<Long, Long, Long>> mShareEventsList = + new HashSet<CalendarUtils.Triple<Long, Long, Long>>(); + + private ArrayList<EventInfoFragment> mEventDataFragments = new ArrayList<EventInfoFragment>(); + + private ActionBar mActionBar; + private AgendaFragment mAgendaFragment; + private long mStartMillis; + private ArrayList<Uri> mFileUris = new ArrayList<Uri>(); + private int mNumQueriesCompleted; + private boolean mShouldSelectSingleEvent; + private Resources mResources; + private String mNoSelectionMsg; + + @Override + public void onEventShared(CalendarUtils.Triple<Long, Long, Long> eventInfo) { + if (eventInfo.first < 0) return; + + mShareEventsList.add(eventInfo); + updateSubtitle(); + } + + @Override + public void onEventRemoval(long eventId) { + if (eventId < 0) return; + + CalendarUtils.Triple<Long, Long, Long> eventInfo = + new CalendarUtils.Triple<Long, Long, Long>(eventId, 0l, 0l); + mShareEventsList.remove(eventInfo); + updateSubtitle(); + } + + @Override + public void onResetShareList() { + // undo + mShareEventsList.clear(); + updateSubtitle(); + } + + public void updateSubtitle() { + int listSize = mShareEventsList.size(); + String subtitle; + // workaround for Android's poor pluralization treatment of 'zero' quantity in English + if (listSize == 0) { + subtitle = mNoSelectionMsg; + } else { + subtitle = mResources.getQuantityString(R.plurals.events_selected, + listSize, listSize); + } + mActionBar.setSubtitle(subtitle); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + Bundle args = intent.getExtras(); + if (args != null) { + mShouldSelectSingleEvent = args.getBoolean(EXTRA_LIMIT_TO_ONE_EVENT, false); + } + + mStartMillis = System.currentTimeMillis(); + setContentView(R.layout.simple_frame_layout); + mResources = getResources(); + + // create agenda fragment displaying the list of calendar events + FragmentManager fragmentManager = getFragmentManager(); + FragmentTransaction ft = fragmentManager.beginTransaction(); + mAgendaFragment = new AgendaFragment(mStartMillis, false, true); + mAgendaFragment.setShareModeOptions(this, mShouldSelectSingleEvent); + ft.replace(R.id.main_frame, mAgendaFragment); + ft.commit(); + + mActionBar = getActionBar(); + mActionBar.setDisplayShowTitleEnabled(true); + String title = mShouldSelectSingleEvent ? + mResources.getQuantityString(R.plurals.select_events_to_share, 1) : + mResources.getQuantityString(R.plurals.select_events_to_share, 2) ; + + mActionBar.setTitle(title); + mNoSelectionMsg = mResources.getString(R.string.no_events_selected); + updateSubtitle(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.share_event_title_bar, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case(R.id.action_done): + generateEventData(); + evalIfComplete(); + break; + + case (R.id.action_cancel): + setResultAndFinish(false); + break; + + } + return super.onOptionsItemSelected(item); + } + + // called after the data fragment's is done loading + public void queryComplete() { + mNumQueriesCompleted++; + evalIfComplete(); + } + + // used load event information for the list of selected events + private void generateEventData() { + for (CalendarUtils.Triple<Long, Long, Long> event : mShareEventsList) { + + long eventId = event.first; + long eventStartMillis = event.second; + long eventEndMillis = event.third; + + // EventInfoFragment just serves as a data fragment and is initialized with + // default arguments for parameters that don't affect model loading + EventInfoFragment eif = new EventInfoFragment(this, eventId, eventStartMillis, + eventEndMillis, CalendarContract.Attendees.ATTENDEE_STATUS_NONE, false, 0, + null); + eif.launchInNonUiMode(); + eif.startQueryingData(this); + eif.setQueryCompleteRunnable(new Runnable() { + @Override + public void run() { + // indicate model loading is complete + queryComplete(); + } + }); + + mEventDataFragments.add(eif); + } + + } + + // generates the vcs files if the data for selected events has been successfully queried + private void evalIfComplete() { + if (mNumQueriesCompleted != 0 && mNumQueriesCompleted == mEventDataFragments.size()) { + + for(EventInfoFragment event : mEventDataFragments) { + try { + // generate vcs file + VCalendar calendar = event.generateVCalendar(); + // event title serves as the file name prefix + String filePrefix = calendar.getFirstEvent().getProperty(VEvent.SUMMARY); + if (filePrefix == null || filePrefix.length() < 3) { + // default to a generic filename if event title doesn't qualify + // prefix length constraint is imposed by File#createTempFile + filePrefix = "invite"; + } + + filePrefix = filePrefix.replaceAll("\\W+", " "); + + if (!filePrefix.endsWith(" ")) { + filePrefix += " "; + } + File dir = getExternalCacheDir(); + File inviteFile = IcalendarUtils.createTempFile(filePrefix, ".vcs", dir); + inviteFile.setReadable(true, false); // set world-readable + if (IcalendarUtils.writeCalendarToFile(calendar, inviteFile)) { + mFileUris.add(Uri.fromFile(inviteFile)); + } + } catch (IOException ioe) { + break; + } + } + + setResultAndFinish(true); + + } else if (mEventDataFragments.size() < 1) { // if no events have been selected + setResultAndFinish(false); + } + } + + private void setResultAndFinish(boolean isResultAvailable) { + if (isResultAvailable) { + Intent intent = new Intent(); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, mFileUris); + setResult(RESULT_OK, intent); + finish(); + } else { + setResult(RESULT_CANCELED); + finish(); + } + } +} diff --git a/src/com/android/calendar/agenda/AgendaAdapter.java b/src/com/android/calendar/agenda/AgendaAdapter.java index 9e492832..0b42bb9d 100644 --- a/src/com/android/calendar/agenda/AgendaAdapter.java +++ b/src/com/android/calendar/agenda/AgendaAdapter.java @@ -26,6 +26,7 @@ import android.text.format.DateUtils; import android.text.format.Time; import android.view.View; import android.view.ViewGroup; +import android.widget.CheckBox; import android.widget.LinearLayout; import android.widget.ResourceCursorAdapter; import android.widget.TextView; @@ -78,6 +79,7 @@ public class AgendaAdapter extends ResourceCursorAdapter { boolean allDay; boolean grayed; int julianDay; + CheckBox selectedForSharing; } public AgendaAdapter(Context context, int resource) { @@ -125,6 +127,7 @@ public class AgendaAdapter extends ResourceCursorAdapter { view.findViewById(R.id.agenda_item_text_container); holder.selectedMarker = view.findViewById(R.id.selected_marker); holder.colorChip = (ColorChipView)view.findViewById(R.id.agenda_item_color); + holder.selectedForSharing = (CheckBox) view.findViewById(R.id.shareCheckbox); } holder.startTimeMilli = cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN); diff --git a/src/com/android/calendar/agenda/AgendaFragment.java b/src/com/android/calendar/agenda/AgendaFragment.java index ff5c47cc..6328bd63 100644 --- a/src/com/android/calendar/agenda/AgendaFragment.java +++ b/src/com/android/calendar/agenda/AgendaFragment.java @@ -41,6 +41,7 @@ import com.android.calendar.CalendarController.ViewType; import com.android.calendar.EventInfoFragment; import com.android.calendar.GeneralPreferences; import com.android.calendar.R; +import com.android.calendar.CalendarUtils.ShareEventListener; import com.android.calendar.StickyHeaderListView; import com.android.calendar.Utils; @@ -71,8 +72,9 @@ public class AgendaFragment extends Fragment implements CalendarController.Event private AgendaWindowAdapter mAdapter = null; private boolean mForceReplace = true; private long mLastShownEventId = -1; - - + private boolean mLaunchedInShareMode; + private boolean mShouldSelectSingleEvent; + private ShareEventListener mShareEventListener; // Tracks the time of the top visible view in order to send UPDATE_TITLE messages to the action // bar. @@ -87,13 +89,17 @@ public class AgendaFragment extends Fragment implements CalendarController.Event }; public AgendaFragment() { - this(0, false); + this(0, false, false); } + public AgendaFragment(long timeMillis, boolean usedForSearch) { + this(0, usedForSearch, false); + } // timeMillis - time of first event to show // usedForSearch - indicates if this fragment is used in the search fragment - public AgendaFragment(long timeMillis, boolean usedForSearch) { + // inShareMode - indicates whether the fragment was started to share calendar events + public AgendaFragment(long timeMillis, boolean usedForSearch, boolean inShareMode) { mInitialTimeMillis = timeMillis; mTime = new Time(); mLastHandledEventTime = new Time(); @@ -105,6 +111,7 @@ public class AgendaFragment extends Fragment implements CalendarController.Event } mLastHandledEventTime.set(mTime); mUsedForSearch = usedForSearch; + mLaunchedInShareMode = inShareMode; } @Override @@ -142,7 +149,6 @@ public class AgendaFragment extends Fragment implements CalendarController.Event public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - int screenWidth = mActivity.getResources().getDisplayMetrics().widthPixels; View v = inflater.inflate(R.layout.agenda_fragment, null); @@ -168,14 +174,24 @@ public class AgendaFragment extends Fragment implements CalendarController.Event if (lv != null) { Adapter a = mAgendaListView.getAdapter(); lv.setAdapter(a); + if (a instanceof HeaderViewListAdapter) { mAdapter = (AgendaWindowAdapter) ((HeaderViewListAdapter)a).getWrappedAdapter(); + if (mLaunchedInShareMode) { + mAdapter.launchInShareMode(true, mShouldSelectSingleEvent); + mAgendaListView.launchInShareMode(true, mShouldSelectSingleEvent); + if (mShareEventListener != null) { + mAgendaListView.setShareEventListener(mShareEventListener); + } + } lv.setIndexer(mAdapter); lv.setHeaderHeightListener(mAdapter); + } else if (a instanceof AgendaWindowAdapter) { mAdapter = (AgendaWindowAdapter)a; lv.setIndexer(mAdapter); lv.setHeaderHeightListener(mAdapter); + } else { Log.wtf(TAG, "Cannot find HeaderIndexer for StickyHeaderListView"); } @@ -208,6 +224,13 @@ public class AgendaFragment extends Fragment implements CalendarController.Event return v; } + // configure share mode launch options + public void setShareModeOptions(ShareEventListener listener, boolean selectSingleEvent) { + mShareEventListener = listener; + mShouldSelectSingleEvent = selectSingleEvent; + + } + @Override public void onResume() { super.onResume(); diff --git a/src/com/android/calendar/agenda/AgendaListView.java b/src/com/android/calendar/agenda/AgendaListView.java index 6cfc7e5b..f3270e66 100644 --- a/src/com/android/calendar/agenda/AgendaListView.java +++ b/src/com/android/calendar/agenda/AgendaListView.java @@ -20,6 +20,8 @@ import com.android.calendar.CalendarController; import com.android.calendar.CalendarController.EventType; import com.android.calendar.DeleteEventHelper; import com.android.calendar.R; +import com.android.calendar.CalendarUtils.ShareEventListener; +import com.android.calendar.CalendarUtils.Triple; import com.android.calendar.Utils; import com.android.calendar.agenda.AgendaAdapter.ViewHolder; import com.android.calendar.agenda.AgendaWindowAdapter.DayAdapterInfo; @@ -51,6 +53,9 @@ public class AgendaListView extends ListView implements OnItemClickListener { private Time mTime; private boolean mShowEventDetailsWithAgenda; private Handler mHandler = null; + private boolean mLaunchedInShareMode; + private boolean mShouldSelectSingleEvent; + private ShareEventListener mShareEventListener; private final Runnable mTZUpdater = new Runnable() { @Override @@ -86,6 +91,12 @@ public class AgendaListView extends ListView implements OnItemClickListener { initView(context); } + // "share mode" is off by default + public void launchInShareMode(boolean inShareMode, boolean selectSingleEvent) { + mLaunchedInShareMode = true; + mShouldSelectSingleEvent = selectSingleEvent; + } + private void initView(Context context) { mContext = context; mTimeZone = Utils.getTimeZone(context, mTZUpdater); @@ -108,6 +119,10 @@ public class AgendaListView extends ListView implements OnItemClickListener { mHandler = new Handler(); } + public void setShareEventListener(ShareEventListener listener) { + mShareEventListener = listener; + } + // Sets a thread to run every EVENT_UPDATE_TIME in order to update the list // with grayed out past events private void setPastEventsUpdater() { @@ -197,10 +212,30 @@ public class AgendaListView extends ListView implements OnItemClickListener { endTime = Utils.convertAlldayLocalToUTC(mTime, endTime, mTimeZone); } mTime.set(startTime); - CalendarController controller = CalendarController.getInstance(mContext); - controller.sendEventRelatedEventWithExtra(this, EventType.VIEW_EVENT, item.id, - startTime, endTime, 0, 0, CalendarController.EventInfo.buildViewExtraLong( - Attendees.ATTENDEE_STATUS_NONE, item.allDay), holderStartTime); + + // divert click action to either a ShareEventListener or the Controller + if (mShareEventListener != null && mLaunchedInShareMode) { + long viewId = ((ViewHolder) holder).instanceId; + + if (mWindowAdapter.isEventInShareList(viewId)) { + Triple<Long, Long, Long> eventinfo = + new Triple<Long, Long, Long>(item.id, item.begin, item.end); + if (mShouldSelectSingleEvent) mShareEventListener.onResetShareList(); + mShareEventListener.onEventShared(eventinfo); + + } else { + // submit event removal + mShareEventListener.onEventRemoval(item.id); + } + + } else { + CalendarController controller = CalendarController.getInstance(mContext); + controller.sendEventRelatedEventWithExtra(this, EventType.VIEW_EVENT, item.id, + startTime, endTime, 0, 0, + CalendarController.EventInfo.buildViewExtraLong( + Attendees.ATTENDEE_STATUS_NONE, item.allDay), + holderStartTime); + } } } } diff --git a/src/com/android/calendar/agenda/AgendaWindowAdapter.java b/src/com/android/calendar/agenda/AgendaWindowAdapter.java index 9fd59f0f..97822f98 100644 --- a/src/com/android/calendar/agenda/AgendaWindowAdapter.java +++ b/src/com/android/calendar/agenda/AgendaWindowAdapter.java @@ -50,6 +50,7 @@ import com.android.calendar.Utils; import java.util.Date; import java.util.Formatter; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.Locale; @@ -74,7 +75,7 @@ Check for leaks and excessive allocations */ public class AgendaWindowAdapter extends BaseAdapter - implements StickyHeaderListView.HeaderIndexer, StickyHeaderListView.HeaderHeightListener{ + implements StickyHeaderListView.HeaderIndexer, StickyHeaderListView.HeaderHeightListener { static final boolean BASICLOG = false; static final boolean DEBUGLOG = false; @@ -228,6 +229,10 @@ public class AgendaWindowAdapter extends BaseAdapter private final int mSelectedItemTextColor; private final float mItemRightMargin; + private boolean mLaunchedInShareMode; + private boolean mShouldSelectSingleEvent; + private HashSet<Long> mSharedEvents = new HashSet<Long>(); + // Types of Query private static final int QUERY_TYPE_OLDER = 0; // Query for older events private static final int QUERY_TYPE_NEWER = 1; // Query for newer events @@ -369,6 +374,12 @@ public class AgendaWindowAdapter extends BaseAdapter mAgendaListView.addHeaderView(mHeaderView); } + // "share mode" is off by default + public void launchInShareMode(boolean inShareMode, boolean selectSingleEvent) { + mLaunchedInShareMode = inShareMode; + mShouldSelectSingleEvent = selectSingleEvent; + } + // Method in Adapter @Override public int getViewTypeCount() { @@ -485,6 +496,25 @@ public class AgendaWindowAdapter extends BaseAdapter pastPresentDivider.setVisibility(View.GONE); } } + + if (mLaunchedInShareMode) { + Object tag = v.getTag(); + if (tag instanceof AgendaAdapter.ViewHolder) { + AgendaAdapter.ViewHolder vh = (AgendaAdapter.ViewHolder) tag; + + // toggle visibility of share checkbox + vh.selectedForSharing.setVisibility(View.VISIBLE); + vh.selectedForSharing.setClickable(false); + + // set 'checked' status + if (mSharedEvents.contains(vh.instanceId)) { + vh.selectedForSharing.setChecked(true); + } else { + vh.selectedForSharing.setChecked(false); + } + } + } + } else { // TODO Log.e(TAG, "BUG: getAdapterInfoByPosition returned null!!! " + position); @@ -1310,10 +1340,32 @@ public class AgendaWindowAdapter extends BaseAdapter Object vh = v.getTag(); if (vh instanceof AgendaAdapter.ViewHolder) { mSelectedVH = (AgendaAdapter.ViewHolder) vh; + boolean datasetChanged = false; + + if (mLaunchedInShareMode) { + + if (mShouldSelectSingleEvent) { + mSharedEvents.clear(); + mSharedEvents.add(mSelectedVH.instanceId); + } else { + + if (mSharedEvents.contains(mSelectedVH.instanceId)) { + mSharedEvents.remove(mSelectedVH.instanceId); + } else { + mSharedEvents.add(mSelectedVH.instanceId); + } + + } + + datasetChanged = true; + } + if (mSelectedInstanceId != mSelectedVH.instanceId) { mSelectedInstanceId = mSelectedVH.instanceId; - notifyDataSetChanged(); + datasetChanged = true; } + + if (datasetChanged) notifyDataSetChanged(); } } } @@ -1397,6 +1449,10 @@ public class AgendaWindowAdapter extends BaseAdapter return -1; } + public boolean isEventInShareList(long id) { + return mSharedEvents.contains(id); + } + @Override public void OnHeaderHeightChanged(int height) { mStickyHeaderSize = height; diff --git a/src/com/android/calendar/icalendar/VCalendar.java b/src/com/android/calendar/icalendar/VCalendar.java index 9f422e6f..140d9358 100644 --- a/src/com/android/calendar/icalendar/VCalendar.java +++ b/src/com/android/calendar/icalendar/VCalendar.java @@ -52,11 +52,11 @@ public class VCalendar { * Add specified property * @param property * @param value - * @return */ public boolean addProperty(String property, String value) { - // since all the required mProperties are unary (only one can exist) , taking a shortcut here - // when multiples of a property can exist , enforce that here .. cleverly + // since all the required mProperties are unary (only one can exist) , taking a shortcut + // here + // TODO: when multiple attributes of a property can exist , enforce that here if (sPropertyList.containsKey(property) && value != null) { mProperties.put(property, IcalendarUtils.cleanseString(value)); return true; @@ -73,16 +73,25 @@ public class VCalendar { } /** - * - * @return + * Returns all the events that are part of this calendar */ public LinkedList<VEvent> getAllEvents() { return mEvents; } /** + * Returns the first event of the calendar + */ + public VEvent getFirstEvent() { + if (mEvents != null && mEvents.size() > 0) { + return mEvents.get(0); + } else { + return null; + } + } + + /** * Returns the iCal representation of the calendar and all of its inherent components - * @return */ public String getICalFormattedString() { StringBuilder output = new StringBuilder(); |