Skip to content

Commit

Permalink
Merge pull request #546 from chris-wolf/develop
Browse files Browse the repository at this point in the history
Added event color (Android) + Updating of calendar color (Android/iOS)
  • Loading branch information
IVLIVS-III authored Sep 25, 2024
2 parents a6352f6 + 7791f59 commit 6a9733c
Show file tree
Hide file tree
Showing 23 changed files with 439 additions and 61 deletions.
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ group 'com.builttoroam.devicecalendar'
version '1.0-SNAPSHOT'

buildscript {
ext.kotlin_version = '1.6.0'
ext.kotlin_version = '1.8.22'
repositories {
google()
mavenCentral()
Expand All @@ -25,7 +25,7 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
compileSdkVersion 33
compileSdkVersion 34

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import com.builttoroam.devicecalendar.common.ErrorCodes.Companion as EC
import com.builttoroam.devicecalendar.common.ErrorMessages.Companion as EM
import org.dmfs.rfc5545.recur.Freq as RruleFreq
import org.dmfs.rfc5545.recur.RecurrenceRule as Rrule
import android.provider.CalendarContract.Colors
import androidx.collection.SparseArrayCompat

private const val RETRIEVE_CALENDARS_REQUEST_CODE = 0
private const val RETRIEVE_EVENTS_REQUEST_CODE = RETRIEVE_CALENDARS_REQUEST_CODE + 1
Expand Down Expand Up @@ -625,6 +627,8 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) :
values.put(Events.DTEND, end)
values.put(Events.EVENT_END_TIMEZONE, endTimeZone)
values.put(Events.DURATION, duration)
values.put(Events.EVENT_COLOR_KEY, event.eventColorKey)
values.put(Events.EVENT_COLOR, event.eventColor)
return values
}

Expand Down Expand Up @@ -938,6 +942,8 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) :
val endTimeZone = cursor.getString(Cst.EVENT_PROJECTION_END_TIMEZONE_INDEX)
val availability = parseAvailability(cursor.getInt(Cst.EVENT_PROJECTION_AVAILABILITY_INDEX))
val eventStatus = parseEventStatus(cursor.getInt(Cst.EVENT_PROJECTION_STATUS_INDEX))
val eventColor = cursor.getInt(Cst.EVENT_PROJECTION_EVENT_COLOR_INDEX)
val eventColorKey = cursor.getInt(Cst.EVENT_PROJECTION_EVENT_COLOR_KEY_INDEX)
val event = Event()
event.eventTitle = title ?: "New Event"
event.eventId = eventId.toString()
Expand All @@ -953,6 +959,8 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) :
event.eventEndTimeZone = endTimeZone
event.availability = availability
event.eventStatus = eventStatus
event.eventColor = if (eventColor == 0) null else eventColor
event.eventColorKey = if (eventColorKey == 0) null else eventColorKey

return event
}
Expand Down Expand Up @@ -1125,6 +1133,73 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) :
return reminders
}

/**
* load available event colors for the given account name
* unable to find official documentation, so logic is based on https://android.googlesource.com/platform/packages/apps/Calendar.git/+/refs/heads/pie-release/src/com/android/calendar/EventInfoFragment.java
**/
private fun retrieveColors(accountName: String, colorType: Int): List<Pair<Int, Int>> {
val contentResolver: ContentResolver? = _context?.contentResolver
val uri: Uri = Colors.CONTENT_URI
val colors = mutableListOf<Int>()
val displayColorKeyMap = SparseArrayCompat<Int>()

val projection = arrayOf(
Colors.COLOR,
Colors.COLOR_KEY,
)

// load only event colors for the given account name
val selection = "${Colors.COLOR_TYPE} = ? AND ${Colors.ACCOUNT_NAME} = ?"
val selectionArgs = arrayOf(colorType.toString(), accountName)


val cursor: Cursor? = contentResolver?.query(uri, projection, selection, selectionArgs, null)
cursor?.use {
while (it.moveToNext()) {
val color = it.getInt(it.getColumnIndexOrThrow(Colors.COLOR))
val colorKey = it.getInt(it.getColumnIndexOrThrow(Colors.COLOR_KEY))
displayColorKeyMap.put(color, colorKey);
colors.add(color)
}
cursor.close();
// sort colors by colorValue, since they are loaded unordered
colors.sortWith(HsvColorComparator())
}
return colors.map { Pair(it, displayColorKeyMap[it]!! ) }.toList()
}

fun retrieveEventColors(accountName: String): List<Pair<Int, Int>> {
return retrieveColors(accountName, Colors.TYPE_EVENT)
}
fun retrieveCalendarColors(accountName: String): List<Pair<Int, Int>> {
return retrieveColors(accountName, Colors.TYPE_CALENDAR)
}

fun updateCalendarColor(calendarId: Long, newColorKey: Int?, newColor: Int?): Boolean {
val contentResolver: ContentResolver? = _context?.contentResolver
val uri: Uri = ContentUris.withAppendedId(CalendarContract.Calendars.CONTENT_URI, calendarId)
val values = ContentValues().apply {
put(CalendarContract.Calendars.CALENDAR_COLOR_KEY, newColorKey)
put(CalendarContract.Calendars.CALENDAR_COLOR, newColor)
}
val rows = contentResolver?.update(uri, values, null, null)
return (rows ?: 0) > 0
}

/**
* Compares colors based on their hue values in the HSV color space.
* https://android.googlesource.com/platform/prebuilts/fullsdk/sources/+/refs/heads/androidx-compose-integration-release/android-34/com/android/colorpicker/HsvColorComparator.java
*/
private class HsvColorComparator : Comparator<Int> {
override fun compare(color1: Int, color2: Int): Int {
val hsv1 = FloatArray(3)
val hsv2 = FloatArray(3)
Color.colorToHSV(color1, hsv1)
Color.colorToHSV(color2, hsv2)
return hsv1[0].compareTo(hsv2[0])
}
}

@Synchronized
private fun generateUniqueRequestCodeAndCacheParameters(parameters: CalendarMethodsParametersCacheModel): Int {
// TODO we can ran out of Int's at some point so this probably should re-use some of the freed ones
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ private const val DELETE_EVENT_INSTANCE_METHOD = "deleteEventInstance"
private const val CREATE_OR_UPDATE_EVENT_METHOD = "createOrUpdateEvent"
private const val CREATE_CALENDAR_METHOD = "createCalendar"
private const val DELETE_CALENDAR_METHOD = "deleteCalendar"
private const val RETRIEVE_EVENT_COLORS_METHOD = "retrieveEventColors"
private const val RETRIEVE_CALENDAR_COLORS_METHOD = "retrieveCalendarColors"
private const val UPDATE_CALENDAR_COLOR = "updateCalendarColor"

// Method arguments
private const val CALENDAR_ID_ARGUMENT = "calendarId"
private const val CALENDAR_NAME_ARGUMENT = "calendarName"
private const val CALENDAR_ACCOUNT_NAME_ARGUMENT = "accountName"
private const val START_DATE_ARGUMENT = "startDate"
private const val END_DATE_ARGUMENT = "endDate"
private const val EVENT_IDS_ARGUMENT = "eventIds"
Expand Down Expand Up @@ -66,6 +70,8 @@ private const val LOCAL_ACCOUNT_NAME_ARGUMENT = "localAccountName"
private const val EVENT_AVAILABILITY_ARGUMENT = "availability"
private const val ATTENDANCE_STATUS_ARGUMENT = "attendanceStatus"
private const val EVENT_STATUS_ARGUMENT = "eventStatus"
private const val EVENT_COLOR_KEY_ARGUMENT = "eventColorKey"
private const val CALENDAR_COLOR_KEY_ARGUMENT = "calendarColorKey"

class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {

Expand Down Expand Up @@ -171,6 +177,35 @@ class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
val calendarId = call.argument<String>(CALENDAR_ID_ARGUMENT)
_calendarDelegate.deleteCalendar(calendarId!!, result)
}
RETRIEVE_EVENT_COLORS_METHOD -> {
val accountName = call.argument<String>(CALENDAR_ACCOUNT_NAME_ARGUMENT)
if (accountName == null) {
result.success(intArrayOf())
return;
}
val colors = _calendarDelegate.retrieveEventColors(accountName!!, )
result.success(colors.map { listOf(it.first, it.second) })
}
RETRIEVE_CALENDAR_COLORS_METHOD -> {
val accountName = call.argument<String>(CALENDAR_ACCOUNT_NAME_ARGUMENT)
if (accountName == null) {
result.success(intArrayOf())
return;
}
val colors = _calendarDelegate.retrieveCalendarColors(accountName)
result.success(colors.map { listOf(it.first, it.second) })
}
UPDATE_CALENDAR_COLOR -> {
val calendarId = call.argument<Number>(CALENDAR_ID_ARGUMENT)?.toLong()
if (calendarId == null) {
result.success(false)
return
}
val newColorKey = (call.argument<Number>(CALENDAR_COLOR_KEY_ARGUMENT))?.toInt()
val newColor = (call.argument<Number>(CALENDAR_COLOR_ARGUMENT))?.toInt()
val success = _calendarDelegate.updateCalendarColor(calendarId, newColorKey, newColor)
result.success(success)
}
else -> {
result.notImplemented()
}
Expand All @@ -192,6 +227,7 @@ class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
event.eventURL = call.argument<String>(EVENT_URL_ARGUMENT)
event.availability = parseAvailability(call.argument<String>(EVENT_AVAILABILITY_ARGUMENT))
event.eventStatus = parseEventStatus(call.argument<String>(EVENT_STATUS_ARGUMENT))
event.eventColorKey = call.argument<Int>(EVENT_COLOR_KEY_ARGUMENT)

if (call.hasArgument(RECURRENCE_RULE_ARGUMENT) && call.argument<Map<String, Any>>(
RECURRENCE_RULE_ARGUMENT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class Constants {
const val EVENT_PROJECTION_END_TIMEZONE_INDEX: Int = 12
const val EVENT_PROJECTION_AVAILABILITY_INDEX: Int = 13
const val EVENT_PROJECTION_STATUS_INDEX: Int = 14
const val EVENT_PROJECTION_EVENT_COLOR_INDEX: Int = 15
const val EVENT_PROJECTION_EVENT_COLOR_KEY_INDEX: Int = 16

val EVENT_PROJECTION: Array<String> = arrayOf(
CalendarContract.Instances.EVENT_ID,
Expand All @@ -66,7 +68,9 @@ class Constants {
CalendarContract.Events.EVENT_TIMEZONE,
CalendarContract.Events.EVENT_END_TIMEZONE,
CalendarContract.Events.AVAILABILITY,
CalendarContract.Events.STATUS
CalendarContract.Events.STATUS,
CalendarContract.Events.EVENT_COLOR,
CalendarContract.Events.EVENT_COLOR_KEY
)

const val EVENT_INSTANCE_DELETION_ID_INDEX: Int = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ class Event {
var reminders: MutableList<Reminder> = mutableListOf()
var availability: Availability? = null
var eventStatus: EventStatus? = null
var eventColor: Int? = null
var eventColorKey: Int? = null
}
4 changes: 2 additions & 2 deletions example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
compileSdkVersion 32
compileSdkVersion 34
ndkVersion '22.1.7171670'

sourceSets {
Expand All @@ -30,7 +30,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.builttoroam.devicecalendarexample"
minSdkVersion 19
minSdkVersion flutter.minSdkVersion
targetSdkVersion 31
versionCode 1
versionName "1.0"
Expand Down
2 changes: 1 addition & 1 deletion example/android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.6.0'
ext.kotlin_version = '1.8.22'
repositories {
google()
mavenCentral()
Expand Down
28 changes: 28 additions & 0 deletions example/lib/presentation/color_picker_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class ColorPickerDialog {
static Future<Color?> selectColorDialog(List<Color> colors, BuildContext context) async {
return await showDialog<Color>(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: const Text('Select color'),
children: [
...colors.map((color) =>
SimpleDialogOption(
onPressed: () { Navigator.pop(context, color); },
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color),
),
)
)]
);
}
);
}
}
2 changes: 1 addition & 1 deletion example/lib/presentation/date_time_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class DateTimePicker extends StatelessWidget {

@override
Widget build(BuildContext context) {
final valueStyle = Theme.of(context).textTheme.headline6;
final valueStyle = Theme.of(context).textTheme.titleLarge;
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expand Down
4 changes: 2 additions & 2 deletions example/lib/presentation/event_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'dart:io';

import 'package:device_calendar/device_calendar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_native_timezone/flutter_native_timezone.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:intl/intl.dart';

import 'recurring_event_dialog.dart';
Expand Down Expand Up @@ -313,7 +313,7 @@ class _EventItemState extends State<EventItem> {
void setCurentLocation() async {
String? timezone;
try {
timezone = await FlutterNativeTimezone.getLocalTimezone();
timezone = await FlutterTimezone.getLocalTimezone();
} catch (e) {
debugPrint('Could not get the local timezone');
}
Expand Down
Loading

0 comments on commit 6a9733c

Please sign in to comment.