From dfb6a82c96cbde481204bb29012b7f3591df3178 Mon Sep 17 00:00:00 2001 From: Prashant Saini Date: Thu, 12 Dec 2024 19:33:10 +0530 Subject: [PATCH] feat(android): added new methods in Ti.Calendar.Calendar module for bulk operations (#14149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(android): added new methods in CalendarProxy for bulk operations * chore: use constant properties * fix: add missing properties of `scrolling` event * chore(android): add docs to new methods for Ti.Calendar.Calendar module * fix(android): set exitOnClose defaults to true on root window if not set already * Revert "fix(android): set exitOnClose defaults to true on root window if not set already" This reverts commit e2c4bb975ac9a2a97947870f4e6ced9dca54c8f0. * fix: fix docs formatting * Update android/modules/calendar/src/java/ti/modules/titanium/calendar/CalendarProxy.java Co-authored-by: Michael Gangolf * Update android/modules/calendar/src/java/ti/modules/titanium/calendar/CalendarProxy.java Co-authored-by: Michael Gangolf * Update android/modules/calendar/src/java/ti/modules/titanium/calendar/CalendarProxy.java Co-authored-by: Michael Gangolf * fix: fix docs --------- Co-authored-by: Michael Gangolf Co-authored-by: Hans Knöchel --- .../titanium/calendar/CalendarProxy.java | 145 ++++++++++++++++++ .../titanium/calendar/CalendarUtils.java | 90 +++++++++++ .../modules/titanium/calendar/EventProxy.java | 67 ++------ apidoc/Titanium/Calendar/CalendarProxy.yml | 50 ++++++ apidoc/Titanium/UI/ListView.yml | 24 +++ 5 files changed, 322 insertions(+), 54 deletions(-) create mode 100644 android/modules/calendar/src/java/ti/modules/titanium/calendar/CalendarUtils.java diff --git a/android/modules/calendar/src/java/ti/modules/titanium/calendar/CalendarProxy.java b/android/modules/calendar/src/java/ti/modules/titanium/calendar/CalendarProxy.java index 86082882a18..dd95acc7997 100644 --- a/android/modules/calendar/src/java/ti/modules/titanium/calendar/CalendarProxy.java +++ b/android/modules/calendar/src/java/ti/modules/titanium/calendar/CalendarProxy.java @@ -7,9 +7,13 @@ package ti.modules.titanium.calendar; +import static ti.modules.titanium.calendar.EventProxy.getEventsUri; + import java.util.ArrayList; import java.util.Calendar; import java.util.Date; +import java.util.HashMap; +import java.util.Map; import org.appcelerator.kroll.KrollDict; import org.appcelerator.kroll.KrollProxy; @@ -19,11 +23,17 @@ import android.app.Activity; import android.content.ContentResolver; +import android.content.ContentValues; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Build; +import android.provider.CalendarContract; import android.text.format.DateUtils; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.OperationApplicationException; +import android.os.RemoteException; @Kroll.proxy(parentModule = CalendarModule.class) public class CalendarProxy extends KrollProxy @@ -188,12 +198,147 @@ public EventProxy getEventById(int id) return null; } + @Kroll.method + public EventProxy[] getEventsById(Object args) + { + ArrayList events = new ArrayList<>(); + + if (args instanceof Object[] eventIds && eventIds.length > 0) { + String query = CalendarUtils.prepareQuerySelection("_id", eventIds.length); + String[] queryArgs = CalendarUtils.prepareQueryArguments(eventIds); + + events.addAll(EventProxy.queryEvents(query, queryArgs)); + } + + return events.toArray(new EventProxy[0]); + } + @Kroll.method public EventProxy createEvent(KrollDict data) { return EventProxy.createEvent(this, data); } + @Kroll.method + public EventProxy[] createEvents(Object data) + { + // Validate arguments to be an array. + if (!(data instanceof Object[] dataList && dataList.length > 0)) { + Log.e(TAG, "Argument expected to be an array."); + return null; + } + + // Check for permissions. + ContentResolver contentResolver = TiApplication.getInstance().getContentResolver(); + if (!hasCalendarPermissions()) { + Log.e(TAG, "Calendar permissions are missing."); + return null; + } + + ArrayList operations = new ArrayList<>(); + ArrayList eventProxies = new ArrayList<>(); + Map proxyResultIndexMapping = new HashMap<>(); + + for (int i = 0, firstIndex = 0; i < dataList.length; i++) { + KrollDict krollDict = new KrollDict((HashMap) dataList[i]); + + EventProxy eventProxy = new EventProxy(); + ContentValues contentValues = CalendarUtils.createContentValues(this, krollDict, eventProxy); + + // We cannot pass null data to ContentProviderOperation. + // Necessary to keep track of non-null items later. + if (contentValues == null) { + eventProxies.add(null); + Log.e(TAG, "Event was not created, no title found for event"); + continue; + } + + ContentProviderOperation.Builder builder = ContentProviderOperation + .newInsert(CalendarContract.Events.CONTENT_URI) + .withValues(contentValues); + + operations.add(builder.build()); + eventProxies.add(eventProxy); + + proxyResultIndexMapping.put(i, firstIndex); + firstIndex++; + } + + try { + // Execute the batch operation + ContentProviderResult[] results = contentResolver.applyBatch( + getEventsUri().getAuthority(), + operations + ); + + // Find non-null proxies and map their IDs + for (int proxyIndex : proxyResultIndexMapping.keySet()) { + int proxyIndexInResults = proxyResultIndexMapping.get(proxyIndex); + Uri eventUri = results[proxyIndexInResults].uri; + + if (eventUri != null) { + // Set event id to proxy. + eventProxies.get(proxyIndex).id = eventUri.getLastPathSegment(); + } else { + // Event failed to get a proper URI path, should set to null in this case too. + eventProxies.set(proxyIndex, null); + } + } + + return eventProxies.toArray(new EventProxy[0]); + + } catch (RemoteException | OperationApplicationException e) { + Log.e(TAG, "Batch insert operation failed: " + e.getMessage()); + return null; + } + } + + @Kroll.method + public int deleteEvents(Object args) + { + int deletedCount = 0; + + // Validate arguments to be an array. + if (!(args instanceof Object[] eventIds && eventIds.length > 0)) { + Log.e(TAG, "Argument expected to be an array."); + return deletedCount; + } + + // Check for permissions. + ContentResolver contentResolver = TiApplication.getInstance().getContentResolver(); + if (!hasCalendarPermissions()) { + Log.e(TAG, "Calendar permissions are missing."); + return deletedCount; + } + + String query = CalendarUtils.prepareQuerySelection("_id", eventIds.length); + String[] queryArgs = CalendarUtils.prepareQueryArguments(eventIds); + + ArrayList operations = new ArrayList<>(); + ContentProviderOperation.Builder builder = ContentProviderOperation + .newDelete(CalendarContract.Events.CONTENT_URI) + .withSelection(query, queryArgs); + + operations.add(builder.build()); + + try { + // Execute the batch operation + ContentProviderResult[] results = contentResolver.applyBatch( + getEventsUri().getAuthority(), + operations + ); + + if (results.length > 0 && results[0].count != null) { + deletedCount = results[0].count; + } + + } catch (RemoteException | OperationApplicationException e) { + Log.e(TAG, "Batch deletion failed: " + e.getMessage()); + } + + return deletedCount; + } + @Kroll.getProperty public String getName() { diff --git a/android/modules/calendar/src/java/ti/modules/titanium/calendar/CalendarUtils.java b/android/modules/calendar/src/java/ti/modules/titanium/calendar/CalendarUtils.java new file mode 100644 index 00000000000..a1693d76c02 --- /dev/null +++ b/android/modules/calendar/src/java/ti/modules/titanium/calendar/CalendarUtils.java @@ -0,0 +1,90 @@ +package ti.modules.titanium.calendar; + +import android.content.ContentValues; +import android.provider.CalendarContract; +import android.text.TextUtils; + +import org.appcelerator.kroll.KrollDict; +import org.appcelerator.titanium.TiC; +import org.appcelerator.titanium.util.TiConvert; + +import java.util.Collections; +import java.util.Date; + +public class CalendarUtils +{ + public static final String TAG = "CalendarUtils"; + + // Build the selection string for IN clause. + public static String prepareQuerySelection(String columnName, int limit) + { + return columnName + " IN (" + TextUtils.join(", ", Collections.nCopies(limit, "?")) + ")"; + } + + // Creates String[] for selectionArgs. + public static String[] prepareQueryArguments(Object[] data) + { + String[] queryArgs = new String[data.length]; + for (int i = 0; i < data.length; i++) { + queryArgs[i] = String.valueOf(data[i]); + } + return queryArgs; + } + + public static ContentValues createContentValues(CalendarProxy calendar, KrollDict data, EventProxy event) + { + if (!data.containsKey(TiC.PROPERTY_TITLE)) { + return null; + } + + ContentValues contentValues = new ContentValues(); + contentValues.put("hasAlarm", 1); + contentValues.put("hasExtendedProperties", 1); + + event.title = TiConvert.toString(data, TiC.PROPERTY_TITLE); + contentValues.put(TiC.PROPERTY_TITLE, event.title); + contentValues.put("calendar_id", calendar.getId()); + contentValues.put(CalendarContract.Events.EVENT_TIMEZONE, new Date().toString()); + + if (data.containsKey(TiC.PROPERTY_LOCATION)) { + event.location = TiConvert.toString(data, TiC.PROPERTY_LOCATION); + contentValues.put(CalendarModule.EVENT_LOCATION, event.location); + } + + if (data.containsKey(TiC.PROPERTY_DESCRIPTION)) { + event.description = TiConvert.toString(data, TiC.PROPERTY_DESCRIPTION); + contentValues.put(TiC.PROPERTY_DESCRIPTION, event.description); + } + + if (data.containsKey("begin")) { + event.begin = TiConvert.toDate(data, "begin"); + if (event.begin != null) { + contentValues.put("dtstart", event.begin.getTime()); + } + } + + if (data.containsKey(TiC.PROPERTY_END)) { + event.end = TiConvert.toDate(data, TiC.PROPERTY_END); + if (event.end != null) { + contentValues.put("dtend", event.end.getTime()); + } + } + + if (data.containsKey("allDay")) { + event.allDay = TiConvert.toBoolean(data, "allDay"); + contentValues.put("allDay", event.allDay ? 1 : 0); + } + + if (data.containsKey("hasExtendedProperties")) { + event.hasExtendedProperties = TiConvert.toBoolean(data, "hasExtendedProperties"); + contentValues.put("hasExtendedProperties", event.hasExtendedProperties ? 1 : 0); + } + + if (data.containsKey("hasAlarm")) { + event.hasAlarm = TiConvert.toBoolean(data, "hasAlarm"); + contentValues.put("hasAlarm", event.hasAlarm ? 1 : 0); + } + + return contentValues; + } +} diff --git a/android/modules/calendar/src/java/ti/modules/titanium/calendar/EventProxy.java b/android/modules/calendar/src/java/ti/modules/titanium/calendar/EventProxy.java index 9ad71100316..5a5c8f39b43 100644 --- a/android/modules/calendar/src/java/ti/modules/titanium/calendar/EventProxy.java +++ b/android/modules/calendar/src/java/ti/modules/titanium/calendar/EventProxy.java @@ -55,9 +55,9 @@ public EventProxy() super(); } - public static String getEventsUri() + public static Uri getEventsUri() { - return CalendarProxy.getBaseCalendarUri() + "/events"; + return Events.CONTENT_URI; } public static String getInstancesWhenUri() @@ -72,7 +72,7 @@ public static String getExtendedPropertiesUri() public static ArrayList queryEvents(String query, String[] queryArgs) { - return queryEvents(Uri.parse(getEventsUri()), query, queryArgs, "dtstart ASC"); + return queryEvents(getEventsUri(), query, queryArgs, "dtstart ASC"); } public static ArrayList queryEventsBetweenDates(long date1, long date2, String query, @@ -146,7 +146,7 @@ public void save() contentValues.put(Events.RRULE, ruleToSave); ContentResolver contentResolver = TiApplication.getInstance().getContentResolver(); try { - contentResolver.update(Events.CONTENT_URI, contentValues, Events._ID + "=?", new String[] { id }); + contentResolver.update(getEventsUri(), contentValues, Events._ID + "=?", new String[] { id }); } catch (IllegalArgumentException e) { Log.e(TAG, "Invalid event recurrence rule."); } @@ -203,60 +203,20 @@ public static EventProxy createEvent(CalendarProxy calendar, KrollDict data) if (!CalendarProxy.hasCalendarPermissions()) { return null; } - EventProxy event = new EventProxy(); - ContentValues eventValues = new ContentValues(); - eventValues.put("hasAlarm", 1); - eventValues.put("hasExtendedProperties", 1); + EventProxy event = new EventProxy(); + ContentValues contentValues = CalendarUtils.createContentValues(calendar, data, event); - if (!data.containsKey("title")) { + if (contentValues == null) { Log.e(TAG, "Title was not created, no title found for event"); return null; } - event.title = TiConvert.toString(data, "title"); - eventValues.put("title", event.title); - eventValues.put("calendar_id", calendar.getId()); - eventValues.put(Events.EVENT_TIMEZONE, new Date().toString()); - - if (data.containsKey(TiC.PROPERTY_LOCATION)) { - event.location = TiConvert.toString(data, TiC.PROPERTY_LOCATION); - eventValues.put(CalendarModule.EVENT_LOCATION, event.location); - } - if (data.containsKey("description")) { - event.description = TiConvert.toString(data, "description"); - eventValues.put("description", event.description); - } - if (data.containsKey("begin")) { - event.begin = TiConvert.toDate(data, "begin"); - if (event.begin != null) { - eventValues.put("dtstart", event.begin.getTime()); - } - } - if (data.containsKey("end")) { - event.end = TiConvert.toDate(data, "end"); - if (event.end != null) { - eventValues.put("dtend", event.end.getTime()); - } - } - if (data.containsKey("allDay")) { - event.allDay = TiConvert.toBoolean(data, "allDay"); - eventValues.put("allDay", event.allDay ? 1 : 0); - } - - if (data.containsKey("hasExtendedProperties")) { - event.hasExtendedProperties = TiConvert.toBoolean(data, "hasExtendedProperties"); - eventValues.put("hasExtendedProperties", event.hasExtendedProperties ? 1 : 0); - } - - if (data.containsKey("hasAlarm")) { - event.hasAlarm = TiConvert.toBoolean(data, "hasAlarm"); - eventValues.put("hasAlarm", event.hasAlarm ? 1 : 0); + Uri eventUri = contentResolver.insert(Uri.parse(CalendarProxy.getBaseCalendarUri() + "/events"), contentValues); + if (eventUri == null) { + return null; } - Uri eventUri = contentResolver.insert(Uri.parse(CalendarProxy.getBaseCalendarUri() + "/events"), eventValues); - Log.d("TiEvents", "created event with uri: " + eventUri, Log.DEBUG_MODE); - String eventId = eventUri.getLastPathSegment(); event.id = eventId; @@ -565,13 +525,12 @@ public boolean remove() ContentResolver contentResolver = TiApplication.getInstance().getContentResolver(); try { - Uri deleteUri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, TiConvert.toInt(id)); - contentResolver.delete(deleteUri, null, null); + Uri deleteUri = ContentUris.withAppendedId(getEventsUri(), TiConvert.toInt(id)); + int deletedCount = contentResolver.delete(deleteUri, null, null); + return deletedCount == 1; } catch (IllegalArgumentException e) { return false; } - - return true; } @Override diff --git a/apidoc/Titanium/Calendar/CalendarProxy.yml b/apidoc/Titanium/Calendar/CalendarProxy.yml index a678efc8776..a612444197f 100644 --- a/apidoc/Titanium/Calendar/CalendarProxy.yml +++ b/apidoc/Titanium/Calendar/CalendarProxy.yml @@ -17,6 +17,21 @@ methods: type: Dictionary platforms: [android, iphone, ipad, macos] + - name: createEvents + summary: Creates multiple events at once in this calendar. + description: | + Use this method to bulk-create events for faster performance. + Successful or failed results are returned at the same position as passed in the parameters list. + For failed events, it will return `null` and for successful events. + returns: + type: Array + parameters: + - name: propertiesArray + summary: Array of the event properties + type: Array> + platforms: [android] + since: {android: "12.6.0"} + - name: getEventById summary: Gets the event with the specified identifier. returns: @@ -27,6 +42,41 @@ methods: type: String platforms: [android, iphone, ipad, macos] + - name: getEventsById + summary: Gets multiple events with their specified identifier(s). + description: | + Use this method to bulk-fetch events for faster performance. + Only successful events are returned, so the identifier of events + should be used to compare which events were not fetched successfully. + returns: + type: Array + parameters: + - name: ids + summary: Array of identifiers of events. + type: [Array, Array] + platforms: [android] + since: {android: "12.6.0"} + + - name: deleteEvents + summary: Deletes multiple events with their specified identifier(s). + description: | + Use this method to bulk-delete events for faster performance. + This method only returns the count of successfully deleted events. + If it is important for apps to know whether the event was deleted or not, + either use the [remove()](Titanium.Calendar.Event.remove) method, or + a single identifier that is passed as an array to this method. + If a specified identifier event does not exist, it will not be treated as a count, + so the count range can be in `0 <= count <= ids.length`. + returns: + type: Number + summary: Count of successfully deleted events. + parameters: + - name: ids + summary: Array of identifiers of events. + type: [Array, Array] + platforms: [android] + since: {android: "12.6.0"} + - name: getEventsBetweenDates summary: Gets events that occur between two dates. returns: diff --git a/apidoc/Titanium/UI/ListView.yml b/apidoc/Titanium/UI/ListView.yml index b6ac0a46f03..b2fa371f090 100644 --- a/apidoc/Titanium/UI/ListView.yml +++ b/apidoc/Titanium/UI/ListView.yml @@ -659,6 +659,30 @@ events: summary: | The expected y axis offset when the scrolling action decelerates to a stop. type: Number + + - name: visibleItemCount + summary: The number of visible items in the list view when the event fires. + type: Number + + - name: firstVisibleItem + summary: The first visible item in the list view when the event fires; this item might not be fully visible. May be -1 on iOS. + type: [Object, Number] + + - name: firstVisibleSection + summary: The first visible section in the list view when the event fires. + type: Titanium.UI.ListSection + + - name: firstVisibleItemIndex + summary: | + The index of the first visible item in the list view when the event fires; this item might not be fully visible. + Note: The index is `-1` when there are no items in the . + type: Number + + - name: firstVisibleSectionIndex + summary: | + The index of the first visible section in the list view when the event fires. + Note: The index is `-1` when there are no items in the . + type: Number properties: - name: allowsSelection