Loading app/src/main/java/com/android/calendar/AllInOneActivity.java +148 −3 Original line number Diff line number Diff line /* * 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. Loading @@ -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; Loading Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -234,6 +241,20 @@ public class AllInOneActivity extends AbstractCalendarActivity implements EventH private LinearLayout.LayoutParams mVerticalControlsParams; private AllInOneMenuExtensionsInterface mExtensions = ExtensionsFactory .getAllInOneMenuExtensions(); private List<CalendarEventModel> 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) { Loading @@ -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 Loading Loading @@ -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<CalendarEventModel>) 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<CalendarEventModel> getEventList(Bundle icicle, Intent intent) { final List<CalendarEventModel> eventModelList = new ArrayList<>(); final ArrayList<Bundle> 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() { Loading Loading @@ -657,7 +738,6 @@ public class AllInOneActivity extends AbstractCalendarActivity implements EventH mCalIntentReceiver = Utils.setTimeChangesReceiver(this, mTimeChangesUpdater); } @Override protected void onPause() { super.onPause(); Loading Loading @@ -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) { Loading Loading @@ -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); Loading app/src/main/java/com/android/calendar/CalendarEventModel.java +90 −1 Original line number Diff line number Diff line /* * 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 Loading @@ -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; Loading @@ -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; Loading Loading @@ -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); Loading app/src/main/java/com/android/calendar/EventUtils.java 0 → 100644 +213 −0 Original line number Diff line number Diff line /* * 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 <https://www.gnu.org/licenses/>. * */ 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(); } } app/src/main/java/com/android/calendar/ImportActivity.java +46 −4 Original line number Diff line number Diff line /* * 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 <https://www.gnu.org/licenses/>. * */ package com.android.calendar; import android.app.Activity; Loading Loading @@ -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; Loading Loading @@ -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<VEvent> 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<VEvent> events) { Intent intent = new Intent(this, AllInOneActivity.class); ArrayList<Bundle> 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))); Loading app/src/main/java/com/android/calendar/event/CalendarPickerAdapter.java 0 → 100644 +65 −0 File added.Preview size limit exceeded, changes collapsed. Show changes Loading
app/src/main/java/com/android/calendar/AllInOneActivity.java +148 −3 Original line number Diff line number Diff line /* * 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. Loading @@ -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; Loading Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -234,6 +241,20 @@ public class AllInOneActivity extends AbstractCalendarActivity implements EventH private LinearLayout.LayoutParams mVerticalControlsParams; private AllInOneMenuExtensionsInterface mExtensions = ExtensionsFactory .getAllInOneMenuExtensions(); private List<CalendarEventModel> 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) { Loading @@ -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 Loading Loading @@ -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<CalendarEventModel>) 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<CalendarEventModel> getEventList(Bundle icicle, Intent intent) { final List<CalendarEventModel> eventModelList = new ArrayList<>(); final ArrayList<Bundle> 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() { Loading Loading @@ -657,7 +738,6 @@ public class AllInOneActivity extends AbstractCalendarActivity implements EventH mCalIntentReceiver = Utils.setTimeChangesReceiver(this, mTimeChangesUpdater); } @Override protected void onPause() { super.onPause(); Loading Loading @@ -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) { Loading Loading @@ -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); Loading
app/src/main/java/com/android/calendar/CalendarEventModel.java +90 −1 Original line number Diff line number Diff line /* * 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 Loading @@ -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; Loading @@ -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; Loading Loading @@ -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); Loading
app/src/main/java/com/android/calendar/EventUtils.java 0 → 100644 +213 −0 Original line number Diff line number Diff line /* * 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 <https://www.gnu.org/licenses/>. * */ 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(); } }
app/src/main/java/com/android/calendar/ImportActivity.java +46 −4 Original line number Diff line number Diff line /* * 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 <https://www.gnu.org/licenses/>. * */ package com.android.calendar; import android.app.Activity; Loading Loading @@ -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; Loading Loading @@ -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<VEvent> 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<VEvent> events) { Intent intent = new Intent(this, AllInOneActivity.class); ArrayList<Bundle> 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))); Loading
app/src/main/java/com/android/calendar/event/CalendarPickerAdapter.java 0 → 100644 +65 −0 File added.Preview size limit exceeded, changes collapsed. Show changes