Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added event color (Android) + Updating of calendar color (Android/iOS) #546

Merged
merged 20 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading