diff --git a/app/src/main/java/com/android/calendar/AllInOneActivity.java b/app/src/main/java/com/android/calendar/AllInOneActivity.java index 23804e0e9287f043b00b229b9fa7da9bbc70982f..9b47acbc1dee911c7e88d088ff47fe18511f2274 100644 --- a/app/src/main/java/com/android/calendar/AllInOneActivity.java +++ b/app/src/main/java/com/android/calendar/AllInOneActivity.java @@ -1,6 +1,7 @@ /* * Copyright (C) 2010 The Android Open Source Project * Copyright (C) 2022 The Calyx Institute + * Copyright (C) 2024 MURENA SAS * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,10 +24,11 @@ import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME; import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME; import android.Manifest; -import android.app.AlarmManager; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.ObjectAnimator; +import android.app.AlarmManager; +import android.app.AlertDialog; import android.app.DatePickerDialog; import android.content.AsyncQueryHandler; import android.content.BroadcastReceiver; @@ -83,6 +85,8 @@ import com.android.calendar.CalendarController.EventType; import com.android.calendar.CalendarController.ViewType; import com.android.calendar.agenda.AgendaFragment; import com.android.calendar.alerts.AlertService; +import com.android.calendar.event.CalendarPickerDialogFragment; +import com.android.calendar.event.EditEventHelper; import com.android.calendar.month.MonthByWeekFragment; import com.android.calendar.selectcalendars.SelectVisibleCalendarsFragment; import com.android.calendar.settings.GeneralPreferences; @@ -97,6 +101,7 @@ import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.TimeZone; @@ -104,13 +109,15 @@ import java.util.TimeZone; import ws.xsoh.etar.R; public class AllInOneActivity extends AbstractCalendarActivity implements EventHandler, - OnSharedPreferenceChangeListener, SearchView.OnQueryTextListener, SearchView.OnSuggestionListener, NavigationView.OnNavigationItemSelectedListener { + OnSharedPreferenceChangeListener, SearchView.OnQueryTextListener, SearchView.OnSuggestionListener, NavigationView.OnNavigationItemSelectedListener, CalendarPickerDialogFragment.CalendarPickerDialogListener { + public static final String BUNDLE_KEY_MULTIPLE_EVENTS = "key_multiple_events"; private static final String TAG = "AllInOneActivity"; private static final boolean DEBUG = false; private static final String EVENT_INFO_FRAGMENT_TAG = "EventInfoFragment"; private static final String BUNDLE_KEY_RESTORE_TIME = "key_restore_time"; private static final String BUNDLE_KEY_EVENT_ID = "key_event_id"; private static final String BUNDLE_KEY_RESTORE_VIEW = "key_restore_view"; + private static final String BUNDLE_KEY_RESTORE_MULTIPLE_EVENTS = "key_restore_multiple_events"; private static final int HANDLER_KEY = 0; private static final int PERMISSIONS_REQUEST_WRITE_CALENDAR = 0; private static final int PERMISSIONS_REQUEST_POST_NOTIFICATIONS = 1; @@ -234,6 +241,20 @@ public class AllInOneActivity extends AbstractCalendarActivity implements EventH private LinearLayout.LayoutParams mVerticalControlsParams; private AllInOneMenuExtensionsInterface mExtensions = ExtensionsFactory .getAllInOneMenuExtensions(); + private List mEventList = Collections.emptyList(); + + private void showCalendarPickerDialog() { + FragmentManager fragmentManager = getSupportFragmentManager(); + CalendarPickerDialogFragment fragment = + ((CalendarPickerDialogFragment) fragmentManager.findFragmentByTag(CalendarPickerDialogFragment.FRAGMENT_TAG)); + + if (fragment != null) { + fragment.dismiss(); + } + + fragment = new CalendarPickerDialogFragment(mEventList.size()); + fragment.show(fragmentManager, CalendarPickerDialogFragment.FRAGMENT_TAG); + } @Override protected void onNewIntent(Intent intent) { @@ -254,6 +275,15 @@ public class AllInOneActivity extends AbstractCalendarActivity implements EventH mController.sendEvent(this, EventType.GO_TO, time, time, -1, ViewType.CURRENT); } } + handleEventsOnNewIntent(intent); + } + + private void handleEventsOnNewIntent(Intent intent) { + if (intent.hasExtra(AllInOneActivity.BUNDLE_KEY_MULTIPLE_EVENTS) + && intent.getExtras().containsKey(BUNDLE_KEY_MULTIPLE_EVENTS)) { + Bundle bundle = intent.getExtras(); + handleEvents(bundle, intent); + } } @Override @@ -382,6 +412,57 @@ public class AllInOneActivity extends AbstractCalendarActivity implements EventH prefs.registerOnSharedPreferenceChangeListener(this); mContentResolver = getContentResolver(); + + // Save and restore flow for handling import of multiple events + if (icicle == null) { + handleEvents(icicle, getIntent()); + } else { + mEventList = ((List) icicle.getSerializable(BUNDLE_KEY_RESTORE_MULTIPLE_EVENTS)); + } + } + + private void handleEvents(Bundle bundle, Intent intent) { + mEventList = getEventList(bundle, intent); + if (mEventList.isEmpty()) { + return; + } + + // TODO: 19/08/2024 Decide to show dialog for consecutive .ics import. Now it replaces previous one. + showCalendarPickerDialog(); + } + + private List getEventList(Bundle icicle, Intent intent) { + final List eventModelList = new ArrayList<>(); + + final ArrayList bundles = intent.getParcelableArrayListExtra(AllInOneActivity.BUNDLE_KEY_MULTIPLE_EVENTS); + + if (bundles == null || bundles.isEmpty()) { + return eventModelList; + } + + for (Bundle bundle : bundles) { + long eventId = -1; + Uri data = intent.getData(); + if (data != null) { + try { + eventId = Long.parseLong(data.getLastPathSegment()); + } catch (NumberFormatException e) { + if (DEBUG) { + Log.d(TAG, "Create new event"); + } + } + } else if (icicle != null && icicle.containsKey(BUNDLE_KEY_EVENT_ID)) { + eventId = icicle.getLong(BUNDLE_KEY_EVENT_ID, -1); + } + + final CalendarEventModel eventModel = new CalendarEventModel(getApplicationContext(), bundle); + if (eventId > -1) { + eventModel.mId = eventId; + } + eventModelList.add(eventModel); + } + + return eventModelList; } private void checkAppPermissions() { @@ -657,7 +738,6 @@ public class AllInOneActivity extends AbstractCalendarActivity implements EventH mCalIntentReceiver = Utils.setTimeChangesReceiver(this, mTimeChangesUpdater); } - @Override protected void onPause() { super.onPause(); @@ -698,6 +778,7 @@ public class AllInOneActivity extends AbstractCalendarActivity implements EventH super.onSaveInstanceState(outState); outState.putLong(BUNDLE_KEY_RESTORE_TIME, mController.getTime()); outState.putInt(BUNDLE_KEY_RESTORE_VIEW, mCurrentView); + outState.putSerializable(BUNDLE_KEY_RESTORE_MULTIPLE_EVENTS, new ArrayList<>(mEventList)); if (mCurrentView == ViewType.EDIT) { outState.putLong(BUNDLE_KEY_EVENT_ID, mController.getEventId()); } else if (mCurrentView == ViewType.AGENDA) { @@ -1481,6 +1562,70 @@ public class AllInOneActivity extends AbstractCalendarActivity implements EventH return false; } + @Override + public void onCalendarPicked(long selectedCalendarId, String ownerAccount, String syncAccountName) { + saveEventsInPickedCalendar(selectedCalendarId, ownerAccount, syncAccountName); + } + + @Override + public void onCalendarUnavailable() { + showAddCalendarDialog(); + } + + private void showAddCalendarDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.no_syncable_calendars) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setMessage(R.string.no_calendars_found) + .setPositiveButton(R.string.add_calendar, (dialog, which) -> { + Intent nextIntent = new Intent(this, SettingsActivity.class); + startActivity(nextIntent); + }) + .setNegativeButton(android.R.string.no, null); + + builder.show(); + } + + @Override + public void onCalendarPickerError() { + Toast.makeText(this, R.string.pick_calendar_error_importing_events, Toast.LENGTH_SHORT).show(); + } + + private void saveEventsInPickedCalendar(long selectedCalendarId, String ownerAccount, String syncAccountName) { + final EditEventHelper editEventHelper = new EditEventHelper(this); + + int addedEventcounter = 0; + + for (CalendarEventModel event : mEventList) { + event.mCalendarId = selectedCalendarId; + event.mOwnerAccount = ownerAccount; + event.mSyncAccountName = syncAccountName; + + if (editEventHelper.saveEvent(event, null, 0, null)) { + addedEventcounter++; + } else { + Toast.makeText( + this, + getString(R.string.import_multi_event_single_event_failure_toast, event.mTitle), + Toast.LENGTH_SHORT + ).show(); + } + } + + if (addedEventcounter > 0) { + Toast.makeText(this, getString(R.string.import_multi_event_confirmation_toast), Toast.LENGTH_SHORT).show(); + navigateToFirstEventOccurrence(); + } + } + + private void navigateToFirstEventOccurrence() { + CalendarEventModel calendarEventModel = mEventList.get(0); + long start = calendarEventModel.mStart; + long end = calendarEventModel.mEnd; + CalendarController.getInstance(this).launchViewEvent(-1, start, end, + Attendees.ATTENDEE_STATUS_NONE); + } + private class QueryHandler extends AsyncQueryHandler { public QueryHandler(ContentResolver cr) { super(cr); diff --git a/app/src/main/java/com/android/calendar/CalendarEventModel.java b/app/src/main/java/com/android/calendar/CalendarEventModel.java index 0d40e260e243ad0e364f4bf6fe38564d442384e0..9cac013d6b044b256ed63986df90f6230064e1c2 100644 --- a/app/src/main/java/com/android/calendar/CalendarEventModel.java +++ b/app/src/main/java/com/android/calendar/CalendarEventModel.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2024 MURENA SAS * * 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 @@ -16,9 +17,15 @@ package com.android.calendar; +import static android.provider.CalendarContract.EXTRA_EVENT_ALL_DAY; +import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME; +import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME; + import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.os.Bundle; +import android.provider.CalendarContract; import android.provider.CalendarContract.Attendees; import android.provider.CalendarContract.Calendars; import android.provider.CalendarContract.Events; @@ -31,7 +38,6 @@ import androidx.annotation.Nullable; import com.android.calendar.event.EditEventHelper; import com.android.calendar.event.EventColorCache; import com.android.calendar.event.ExtendedProperty; -import com.android.calendar.icalendar.VEvent; import com.android.calendar.settings.GeneralPreferences; import com.android.common.Rfc822Validator; @@ -149,6 +155,89 @@ public class CalendarEventModel implements Serializable { } } + public CalendarEventModel(Context context, Bundle bundle) { + this(context); + + if (bundle == null) { + return; + } + + final String title = bundle.getString(Events.TITLE); + if (title != null) { + mTitle = title; + } + + final String location = bundle.getString(Events.EVENT_LOCATION); + if (location != null) { + mLocation = location; + } + + final String description = bundle.getString(Events.DESCRIPTION); + if (description != null) { + mDescription = description; + } + + final String url = bundle.getString(ExtendedProperty.URL); + if (url != null) { + mUrl = url; + } + + final int availability = bundle.getInt(Events.AVAILABILITY, -1); + if (availability != -1) { + mAvailability = availability; + mAvailabilityExplicitlySet = true; + } + + final int accessLevel = bundle.getInt(Events.ACCESS_LEVEL, -1); + if (accessLevel != -1) { + mAccessLevel = accessLevel; + } + + final String rrule = bundle.getString(Events.RRULE); + if (!TextUtils.isEmpty(rrule)) { + mRrule = rrule; + } + + final String timezone = bundle.getString(Events.EVENT_TIMEZONE); + if (timezone != null) { + mTimezone = timezone; + } + + final long beginTime = bundle.getLong(EXTRA_EVENT_BEGIN_TIME, -1); + if (beginTime > -1) { + mStart = beginTime; + } + + final long endTime = bundle.getLong(EXTRA_EVENT_END_TIME, -1); + if (endTime > -1) { + mEnd = endTime; + } + + final boolean isAllDay = bundle.getBoolean(EXTRA_EVENT_ALL_DAY, false); + if (isAllDay) { + mAllDay = isAllDay; + } + + final String organizer = bundle.getString(CalendarContract.Events.ORGANIZER, ""); + if (!organizer.isEmpty()) { + mOrganizer = organizer; + } + + final String emails = bundle.getString(Intent.EXTRA_EMAIL); + if (!TextUtils.isEmpty(emails)) { + final String[] emailArray = emails.split("[ ,;]"); + for (String email : emailArray) { + if (!TextUtils.isEmpty(email) && email.contains("@")) { + email = email.trim(); + if (!mAttendeesList.containsKey(email)) { + mAttendeesList.put(email, new Attendee("", email)); + } + } + } + } + } + + public CalendarEventModel(Context context, Intent intent) { this(context); diff --git a/app/src/main/java/com/android/calendar/EventUtils.java b/app/src/main/java/com/android/calendar/EventUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..4fc170ffe12155316cbfcb012b53bc97614e5f00 --- /dev/null +++ b/app/src/main/java/com/android/calendar/EventUtils.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2024 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.android.calendar; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.provider.CalendarContract; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.NonNull; + +import com.android.calendar.event.EditEventActivity; +import com.android.calendar.event.ExtendedProperty; +import com.android.calendar.icalendar.Attendee; +import com.android.calendar.icalendar.IcalendarUtils; +import com.android.calendar.icalendar.VEvent; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.TimeZone; + +import ws.xsoh.etar.R; + +public class EventUtils { + private final static String TAG = EventUtils.class.getName(); + private EventUtils() { + } + + @NonNull + public static Bundle createBundleFromEvent(VEvent event, Context context) { + Bundle bundle = new Bundle(); + + bundle.putString(CalendarContract.Events.TITLE, + IcalendarUtils.uncleanseString(event.getProperty(VEvent.SUMMARY))); + bundle.putString(CalendarContract.Events.EVENT_LOCATION, + IcalendarUtils.uncleanseString(event.getProperty(VEvent.LOCATION))); + bundle.putString(CalendarContract.Events.DESCRIPTION, + IcalendarUtils.uncleanseString(event.getProperty(VEvent.DESCRIPTION))); + bundle.putString(ExtendedProperty.URL, + IcalendarUtils.uncleanseString(event.getProperty(VEvent.URL))); + bundle.putString(CalendarContract.Events.ORGANIZER, + IcalendarUtils.uncleanseString(event.getProperty(VEvent.ORGANIZER))); + bundle.putString(CalendarContract.Events.RRULE, + IcalendarUtils.uncleanseString(event.getProperty(VEvent.RRULE))); + + if (event.mAttendees.size() > 0) { + StringBuilder builder = new StringBuilder(); + for (Attendee attendee : event.mAttendees) { + builder.append(attendee.mEmail); + builder.append(","); + } + bundle.putString(Intent.EXTRA_EMAIL, builder.toString()); + } + + String dtStart = event.getProperty(VEvent.DTSTART); + String dtStartParam = event.getPropertyParameters(VEvent.DTSTART); + if (!TextUtils.isEmpty(dtStart)) { + bundle.putLong(CalendarContract.EXTRA_EVENT_BEGIN_TIME, + getLocalTimeFromString(dtStart, dtStartParam, context)); + } + + String dtEnd = event.getProperty(VEvent.DTEND); + String dtEndParam = event.getPropertyParameters(VEvent.DTEND); + if (dtEnd != null && !TextUtils.isEmpty(dtEnd)) { + bundle.putLong(CalendarContract.EXTRA_EVENT_END_TIME, + getLocalTimeFromString(dtEnd, dtEndParam, context)); + } else { + // Treat start date as end date if un-specified + dtEnd = dtStart; + dtEndParam = dtStartParam; + } + + boolean isAllDay = getLocalTimeFromString(dtEnd, dtEndParam, context) + - getLocalTimeFromString(dtStart, dtStartParam, context) == 24*60*60*1000; + + + if (isTimeStartOfDay(dtStart, dtStartParam, context)) { + bundle.putBoolean(CalendarContract.EXTRA_EVENT_ALL_DAY, isAllDay); + } + //Check if some special property which say it is a "All-Day" event. + + String microsoft_all_day_event = event.getProperty("X-MICROSOFT-CDO-ALLDAYEVENT"); + if (!TextUtils.isEmpty(microsoft_all_day_event) && microsoft_all_day_event.equals("TRUE")) { + bundle.putBoolean(CalendarContract.EXTRA_EVENT_ALL_DAY, true); + } + + bundle.putBoolean(EditEventActivity.EXTRA_READ_ONLY, true); + + return bundle; + } + + private static boolean isTimeStartOfDay(String dtStart, String dtStartParam, Context context) { + // convert to epoch milli seconds + long timeStamp = getLocalTimeFromString(dtStart, dtStartParam, context); + Date date = new Date(timeStamp); + + DateFormat dateFormat = new SimpleDateFormat("HH:mm"); + String dateStr = dateFormat.format(date); + if (dateStr.equals("00:00")) { + return true; + } + return false; + } + + private static long getLocalTimeFromString(String iCalDate, String iCalDateParam, Context context) { + // see https://tools.ietf.org/html/rfc5545#section-3.3.5 + + // FORM #2: DATE WITH UTC TIME, e.g. 19980119T070000Z + if (iCalDate.endsWith("Z")) { + SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + + try { + format.parse(iCalDate); + format.setTimeZone(TimeZone.getDefault()); + return format.getCalendar().getTimeInMillis(); + } catch (ParseException exception) { + Log.e(TAG, "Can't parse iCalDate:", exception); + } + } + + // FORM #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE, e.g. TZID=America/New_York:19980119T020000 + else if (iCalDateParam != null && iCalDateParam.startsWith("TZID=")) { + SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); + String timeZone = iCalDateParam.substring(5).replace("\"", ""); + // This is a pretty hacky workaround to prevent exact parsing of VTimezones. + // It assumes the TZID to be refered to with one of the names recognizable by Java. + // (which are quite a lot, see e.g. http://tutorials.jenkov.com/java-date-time/java-util-timezone.html) + if (Arrays.asList(TimeZone.getAvailableIDs()).contains(timeZone)) { + format.setTimeZone(TimeZone.getTimeZone(timeZone)); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + String convertedTimeZoneId = android.icu.util.TimeZone + .getIDForWindowsID(timeZone, "001"); + if (convertedTimeZoneId != null && !convertedTimeZoneId.equals("")) { + format.setTimeZone(TimeZone.getTimeZone(convertedTimeZoneId)); + } else { + format.setTimeZone(TimeZone.getDefault()); + Toast.makeText( + context, + context.getString(R.string.cal_import_error_time_zone_msg, timeZone), + Toast.LENGTH_SHORT).show(); + } + } else { + format.setTimeZone(TimeZone.getDefault()); + Toast.makeText( + context, + context.getString(R.string.cal_import_error_time_zone_msg, timeZone), + Toast.LENGTH_SHORT).show(); + } + } + try { + format.parse(iCalDate); + return format.getCalendar().getTimeInMillis(); + } catch (ParseException exception) { + Log.e(TAG, "Can't parse iCalDate:", exception); + } + } + + // ONLY DATE, e.g. 20190415 + else if (iCalDateParam != null && iCalDateParam.equals("VALUE=DATE")) { + SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd"); + format.setTimeZone(TimeZone.getDefault()); + + try { + format.parse(iCalDate); + return format.getCalendar().getTimeInMillis(); + } catch (ParseException exception) { + Log.e(TAG, "Can't parse iCalDate:", exception); + } + } + + // FORM #1: DATE WITH LOCAL TIME, e.g. 19980118T230000 + else { + SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); + format.setTimeZone(TimeZone.getDefault()); + + try { + format.parse(iCalDate); + return format.getCalendar().getTimeInMillis(); + } catch (ParseException exception) { + Log.e(TAG, "Can't parse iCalDate:", exception); + } + } + + Toast.makeText(context, context.getString(R.string.cal_import_error_date_msg, iCalDate), Toast.LENGTH_SHORT).show(); + + return System.currentTimeMillis(); + } +} diff --git a/app/src/main/java/com/android/calendar/ImportActivity.java b/app/src/main/java/com/android/calendar/ImportActivity.java index c55f31c6fcb5a8a6dc0f9f91a5602485f9772c52..f97988d084d7a27ff46728471f07b346317ce530 100644 --- a/app/src/main/java/com/android/calendar/ImportActivity.java +++ b/app/src/main/java/com/android/calendar/ImportActivity.java @@ -1,3 +1,21 @@ +/* + * Copyright (C) 2024 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + package com.android.calendar; import android.app.Activity; @@ -25,9 +43,11 @@ import java.io.File; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.LinkedList; +import java.util.List; import java.util.TimeZone; import ws.xsoh.etar.R; @@ -142,15 +162,37 @@ public class ImportActivity extends Activity { return; } - Intent calIntent = new Intent(Intent.ACTION_INSERT); - calIntent.setType("vnd.android.cursor.item/event"); - LinkedList events = calendar.getAllEvents(); - if (events == null) { + if (events == null || events.isEmpty()) { showErrorToast(); return; } + if (events.size() == 1){ + handleSingleEvent(calendar); + } else { + handleMultipleEvents(events); + } + } + + private void handleMultipleEvents(List events) { + Intent intent = new Intent(this, AllInOneActivity.class); + ArrayList bundleList = new ArrayList<>(); + + for (VEvent event : events) { + bundleList.add(EventUtils.createBundleFromEvent(event, this)); + } + + intent.putParcelableArrayListExtra(AllInOneActivity.BUNDLE_KEY_MULTIPLE_EVENTS, bundleList); + startActivity(intent); + + finish(); + } + + private void handleSingleEvent(VCalendar calendar) { + Intent calIntent = new Intent(Intent.ACTION_INSERT); + calIntent.setType("vnd.android.cursor.item/event"); + VEvent firstEvent = calendar.getAllEvents().getFirst(); calIntent.putExtra(CalendarContract.Events.TITLE, IcalendarUtils.uncleanseString(firstEvent.getProperty(VEvent.SUMMARY))); diff --git a/app/src/main/java/com/android/calendar/event/CalendarPickerAdapter.java b/app/src/main/java/com/android/calendar/event/CalendarPickerAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..8c1e25ad2063fe0f15079f9aa49856f035254dd6 --- /dev/null +++ b/app/src/main/java/com/android/calendar/event/CalendarPickerAdapter.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.android.calendar.event; + +import android.content.Context; +import android.database.Cursor; +import android.provider.CalendarContract; +import android.view.View; +import android.widget.ResourceCursorAdapter; +import android.widget.TextView; + +import com.android.calendar.Utils; + +import ws.xsoh.etar.R; + +public class CalendarPickerAdapter extends ResourceCursorAdapter { + + public CalendarPickerAdapter(Context context, int resourceId, Cursor cursor) { + super(context, resourceId, cursor, 0); // No selection flags added to avoid deprecated call + setDropDownViewResource(R.layout.calendars_dropdown_item); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + View colorBar = view.findViewById(R.id.color); + int colorColumn = cursor.getColumnIndexOrThrow(CalendarContract.Calendars.CALENDAR_COLOR); + if (colorBar != null) { + colorBar.setBackgroundColor( + Utils.getDisplayColorFromColor( + context, + cursor.getInt(colorColumn) + ) + ); + } + + int nameColumn = cursor.getColumnIndexOrThrow(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME); + int ownerColumn = cursor.getColumnIndexOrThrow(CalendarContract.Calendars.OWNER_ACCOUNT); + + TextView name = view.findViewById(R.id.calendar_name); + if (name == null) return; + String displayName = cursor.getString(nameColumn); + name.setText(displayName); + + TextView accountName = view.findViewById(R.id.account_name); + if (accountName == null) return; + accountName.setText(cursor.getString(ownerColumn)); + accountName.setVisibility(TextView.VISIBLE); + } +} diff --git a/app/src/main/java/com/android/calendar/event/CalendarPickerDialogFragment.java b/app/src/main/java/com/android/calendar/event/CalendarPickerDialogFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..0f7ca9c7aed4f1ec9e42d62c2a65fbea34f8ed2b --- /dev/null +++ b/app/src/main/java/com/android/calendar/event/CalendarPickerDialogFragment.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2024 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.android.calendar.event; + + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.os.Bundle; +import android.provider.CalendarContract; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; + +import ws.xsoh.etar.R; + +/** + * Allows the user to quickly import a multi event cal file. + */ +public class CalendarPickerDialogFragment extends DialogFragment implements AdapterView.OnItemSelectedListener, CalendarPickerQueryListener { + + public static final String FRAGMENT_TAG = "PICK_CALENDAR_DIALOG"; + public static final int SPINNER_DEFAULT_POSITION = 0; + private static final int TOKEN_CALENDARS = 8; + private static final int EVENT_INDEX_OWNER_ACCOUNT = 2; + private static final int EVENT_INDEX_ACCOUNT_NAME = 11; + private static final String BUNDLE_KEY_RESTORE_SELECTED_SPINNER_ITEM_POSITION = "selected_spinner_item_position"; + private static final String BUNDLE_KEY_RESTORE_NUMBER_OF_EVENTS = "number_of_events"; + private String mSyncAccountName = null; + private long mPickedCalendarId = -1; + private Cursor mCalendarsCursor; + private String mOwnerAccount; + private Spinner mCalendarsSpinner; + private int mSelectedSpinnerItemPosition = SPINNER_DEFAULT_POSITION; + // Instance of class that request to show this dialog and which expect the selected calendar + private CalendarPickerDialogListener calendarPickerDialogListener; + private int mNumberOfEvents; + + public CalendarPickerDialogFragment() { + } + + public CalendarPickerDialogFragment(int numberOfEvents) { + this.mNumberOfEvents = numberOfEvents; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof CalendarPickerDialogListener) { + calendarPickerDialogListener = (CalendarPickerDialogListener) context; + } else { + throw new RuntimeException(context + " must implement CalendarPickerDialogListener"); + } + } + + @Override + public void onDetach() { + calendarPickerDialogListener = null; + super.onDetach(); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + initiateCalendarQueryHandler(); + + // Save and restore flow + if (savedInstanceState != null) { + mSelectedSpinnerItemPosition = savedInstanceState.getInt(BUNDLE_KEY_RESTORE_SELECTED_SPINNER_ITEM_POSITION); + mNumberOfEvents = savedInstanceState.getInt(BUNDLE_KEY_RESTORE_NUMBER_OF_EVENTS); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(BUNDLE_KEY_RESTORE_SELECTED_SPINNER_ITEM_POSITION, mSelectedSpinnerItemPosition); + outState.putInt(BUNDLE_KEY_RESTORE_NUMBER_OF_EVENTS, mNumberOfEvents); + } + + @Override + public void onDestroyView() { + closeCursor(); + super.onDestroyView(); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + // onCreateDialog() gets called before onCreateView(), so the view needs to be created and supplied here + // getView() returns null at this point + View view = LayoutInflater.from(requireContext()).inflate(R.layout.pick_calendar_dialog, null); + + mCalendarsSpinner = view.findViewById(R.id.calendars_spinner); + mCalendarsSpinner.setSelection(mSelectedSpinnerItemPosition); + + TextView dialogMessage = view.findViewById(R.id.pick_calendar_dialog_text); + dialogMessage.setText(getString(R.string.pick_calendar_dialog_text, mNumberOfEvents)); + + return new AlertDialog.Builder(requireActivity()) + .setTitle(R.string.pick_calendar_dialog_title) + .setView(view) + .setPositiveButton(R.string.pick_calendar_dialog_validate, + (dialog, which) -> { + calendarPickerDialogListener.onCalendarPicked(mPickedCalendarId, mOwnerAccount, mSyncAccountName); + dismiss(); + }) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> dismiss()) + .create(); + } + + private void closeCursor() { + if (mCalendarsCursor != null && !mCalendarsCursor.isClosed()) { + mCalendarsCursor.close(); + } + } + + private void initiateCalendarQueryHandler() { + final ContentResolver contentResolver = requireActivity().getContentResolver(); + if (contentResolver == null) { + return; + } + + CalendarPickerQueryHandler calendarPickerQueryHandler = new CalendarPickerQueryHandler(contentResolver, requireActivity(), this); + + calendarPickerQueryHandler.startQuery( + TOKEN_CALENDARS, + null, + CalendarContract.Calendars.CONTENT_URI, + EditEventHelper.CALENDARS_PROJECTION, + EditEventHelper.CALENDARS_WHERE_WRITEABLE_VISIBLE, + null /* selection args */, + null /* sort order */ + ); + } + + public void setCalendarsCursor(Cursor cursor) { + mCalendarsCursor = cursor; + + if (mCalendarsCursor.moveToFirst()) { + mOwnerAccount = mCalendarsCursor.getString(EVENT_INDEX_OWNER_ACCOUNT); + mSyncAccountName = mCalendarsCursor.getString(EVENT_INDEX_ACCOUNT_NAME); + bindCalendarSpinnerUi(); + } + } + + private void bindCalendarSpinnerUi() { + CalendarPickerAdapter adapter = new CalendarPickerAdapter(requireContext(), + R.layout.calendars_spinner_item, + mCalendarsCursor); + + mCalendarsSpinner.setAdapter(adapter); + mCalendarsSpinner.setOnItemSelectedListener(this); + + // Restore Spinner's position if there's any saved on configuration changes + if (mSelectedSpinnerItemPosition != SPINNER_DEFAULT_POSITION) { + mCalendarsSpinner.setSelection(mSelectedSpinnerItemPosition); + } + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + mSelectedSpinnerItemPosition = position; + mPickedCalendarId = id; + } + + @Override + public void onNothingSelected(AdapterView parent) { + mPickedCalendarId = -1; + } + + @Override + public void onCursorAvailable(Cursor cursor) { + setCalendarsCursor(cursor); + } + + @Override + public void onCursorUnavailable() { + dismiss(); + calendarPickerDialogListener.onCalendarUnavailable(); + } + + @Override + public void onCalendarPickerError() { + calendarPickerDialogListener.onCalendarPickerError(); + } + + public interface CalendarPickerDialogListener { + void onCalendarPicked(long selectedCalendarId, String ownerAccount, String syncAccountName); + + void onCalendarUnavailable(); + + void onCalendarPickerError(); + } +} + diff --git a/app/src/main/java/com/android/calendar/event/CalendarPickerQueryHandler.java b/app/src/main/java/com/android/calendar/event/CalendarPickerQueryHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..ac8704651f2377f19857238f030838a904806b89 --- /dev/null +++ b/app/src/main/java/com/android/calendar/event/CalendarPickerQueryHandler.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.android.calendar.event; + +import android.app.Activity; +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.database.Cursor; +import android.database.MatrixCursor; + +import com.android.calendar.Utils; + +import java.lang.ref.WeakReference; + +public class CalendarPickerQueryHandler extends AsyncQueryHandler { + private final WeakReference activityReference; + private final WeakReference listenerReference; + + public CalendarPickerQueryHandler(ContentResolver contentResolver, Activity activity, CalendarPickerQueryListener calendarPickerQueryListener) { + super(contentResolver); + activityReference = new WeakReference<>(activity); + listenerReference = new WeakReference<>(calendarPickerQueryListener); + } + + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + super.onQueryComplete(token, cookie, cursor); + + final CalendarPickerQueryListener queryListener = listenerReference.get(); + + if (queryListener == null) { + return; + } + + if (cursor == null || cursor.getCount() == 0) { + queryListener.onCursorUnavailable(); + return; + } + + final Activity activity = activityReference.get(); + if (activity == null || activity.isFinishing()) { + cursor.close(); + queryListener.onCalendarPickerError(); + return; + } + + try (cursor) { + MatrixCursor matrixCursor = Utils.matrixCursorFromCursor(cursor); + queryListener.onCursorAvailable(matrixCursor); + } + } +} diff --git a/app/src/main/java/com/android/calendar/event/CalendarPickerQueryListener.java b/app/src/main/java/com/android/calendar/event/CalendarPickerQueryListener.java new file mode 100644 index 0000000000000000000000000000000000000000..b8aca376364345f71e84560c6d0873f31c7d3061 --- /dev/null +++ b/app/src/main/java/com/android/calendar/event/CalendarPickerQueryListener.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.android.calendar.event; + +import android.database.Cursor; + +public interface CalendarPickerQueryListener { + void onCursorAvailable(Cursor cursor); + + void onCursorUnavailable(); + + void onCalendarPickerError(); +} diff --git a/app/src/main/res/layout/calendars_dropdown_item.xml b/app/src/main/res/layout/calendars_dropdown_item.xml index fee8b81fc3a28baf34df72c3e7d8ace03ee32aac..8b360224103a445feee9d5b98255a7bd14973738 100644 --- a/app/src/main/res/layout/calendars_dropdown_item.xml +++ b/app/src/main/res/layout/calendars_dropdown_item.xml @@ -15,6 +15,7 @@ --> + tools:text="@tools:sample/lorem/random" + /> + tools:text="@tools:sample/lorem/random" + /> diff --git a/app/src/main/res/layout/pick_calendar_dialog.xml b/app/src/main/res/layout/pick_calendar_dialog.xml new file mode 100644 index 0000000000000000000000000000000000000000..ec058f4c37b3c54703e379b81473bd1cf7c537c9 --- /dev/null +++ b/app/src/main/res/layout/pick_calendar_dialog.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3799adcd559fe7502d072dc04c33425391c1701c..d8c26b0e440e2a88d28d8d8b8127212a5b658d86 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,6 @@ "Responded no." + + + + + OK + + Set calendar + + In which calendar do you want to import these %d events? + + The events have been created successfully. + + Importing %s in your calendar failed + + Couldn\'t add events to the calendar.