feat: add local reminders and owned calendar synchronization

This commit is contained in:
Codex
2026-10-13 00:11:48 +08:00
parent ffd3920c0d
commit 2c4256b880
13 changed files with 978 additions and 8 deletions
+6 -1
View File
@@ -10,6 +10,7 @@ android {
ndkVersion = flutter.ndkVersion
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
@@ -19,7 +20,7 @@ android {
applicationId = "com.example.hepan_everyday"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
minSdk = 24
targetSdk = flutter.targetSdkVersion
// Uses the version code from pubspec.yaml. When using split APKs, 1000 * ABI_VERSION
// is added automatically by Flutter. (https://developer.android.com/studio/build/configure-apk-splits#configure-APK-versions)
@@ -38,6 +39,10 @@ android {
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
@@ -1,8 +1,23 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application
android:label="河畔Everyday"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" android:exported="false" />
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true"
@@ -0,0 +1,127 @@
package com.example.hepan_everyday
import android.Manifest
import android.app.Activity
import android.content.ContentUris
import android.content.ContentValues
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.CalendarContract.Calendars
import android.provider.CalendarContract.Events
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel
import java.util.concurrent.Executors
/** Reads only calendar names and this app's marked events. No calendar reminders are created. */
class CalendarBridge(private val activity: Activity, messenger: BinaryMessenger) {
private val channel = MethodChannel(messenger, "hepan_everyday/calendar")
private val worker = Executors.newSingleThreadExecutor()
private var permissionResult: MethodChannel.Result? = null
private val resolver get() = activity.contentResolver
private val owner get() = activity.packageName
private val code = 18482
private val prefix = "hepan://course/"
private fun allowed() = listOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR)
.all { activity.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED }
init {
channel.setMethodCallHandler { call, result ->
if (call.method == "requestAccess") {
if (allowed()) result.success(true)
else if (permissionResult != null) result.error("BUSY", "日历权限请求进行中", null)
else {
permissionResult = result
activity.requestPermissions(arrayOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR), code)
}
} else if (call.method !in setOf("listCalendars", "ownedEvents", "saveEvent", "deleteEvent")) {
result.notImplemented()
} else worker.execute {
try {
check(allowed()) { "日历权限未获允许" }
val args = (call.arguments as? Map<*, *>) ?: emptyMap<Any, Any>()
val value: Any? = when(call.method) {
"listCalendars" -> calendars()
"ownedEvents" -> ownedEvents(args["calendarId"] as String)
"saveEvent" -> save(args)
"deleteEvent" -> { delete(args["id"] as String, args["key"] as String); null }
else -> null
}
activity.runOnUiThread { result.success(value) }
} catch (_: Exception) {
activity.runOnUiThread { result.error("CALENDAR", "日历操作失败,请检查访问权限及所选日历", null) }
}
}
}
}
fun onPermissionResult(requestCode: Int) {
if (requestCode == code) { permissionResult?.success(allowed()); permissionResult = null }
}
fun close() {
channel.setMethodCallHandler(null)
permissionResult?.success(false); permissionResult = null
worker.shutdown()
}
private fun calendars(): List<Map<String, String>> {
val rows = mutableListOf<Map<String, String>>()
resolver.query(Calendars.CONTENT_URI, arrayOf(Calendars._ID, Calendars.CALENDAR_DISPLAY_NAME),
"${Calendars.CALENDAR_ACCESS_LEVEL} >= ?", arrayOf(Calendars.CAL_ACCESS_CONTRIBUTOR.toString()), null)?.use {
while(it.moveToNext()) rows.add(mapOf("id" to it.getLong(0).toString(), "name" to (it.getString(1) ?: "手机日历")))
} ?: error("日历服务不可用")
return rows
}
private fun marker(key: String) = prefix + Uri.encode(key)
private fun ownedEvents(calendarId: String): List<Map<String, Any>> {
require(calendarId.toLong() > 0)
val rows = mutableListOf<Map<String, Any>>()
resolver.query(Events.CONTENT_URI, arrayOf(Events._ID, Events.CUSTOM_APP_URI),
"${Events.CALENDAR_ID}=? AND ${Events.CUSTOM_APP_PACKAGE}=? AND ${Events.DELETED}=0",
arrayOf(calendarId, owner), null)?.use {
while(it.moveToNext()) {
val uri = it.getString(1) ?: continue
if(uri.startsWith(prefix)) rows.add(mapOf("id" to it.getLong(0).toString(), "key" to Uri.decode(uri.substring(prefix.length)),
"calendarId" to calendarId, "owned" to true))
}
} ?: error("日历服务不可用")
return rows
}
private fun ownership(id: String, key: String, calendarId: String? = null): Boolean {
require(id.toLong() > 0)
resolver.query(ContentUris.withAppendedId(Events.CONTENT_URI, id.toLong()),
arrayOf(Events.CUSTOM_APP_PACKAGE, Events.CUSTOM_APP_URI, Events.CALENDAR_ID, Events.DELETED), null,null,null)?.use {
return it.moveToFirst() && it.getString(0) == owner && it.getString(1) == marker(key) && it.getInt(3)==0 &&
(calendarId == null || it.getLong(2).toString() == calendarId)
}
return false
}
private fun save(args: Map<*, *>): String {
val calendarId = args["calendarId"] as String
check(calendars().any { it["id"] == calendarId }) { "日历不可写入" }
val key = args["key"] as String
require(key.isNotBlank() && key.length<=512)
val start = (args["startMs"] as Number).toLong()
val end = (args["endMs"] as Number).toLong()
require(end>start)
val values = ContentValues().apply {
put(Events.CALENDAR_ID, calendarId.toLong())
put(Events.TITLE, args["title"] as String)
put(Events.EVENT_LOCATION, args["room"] as String)
put(Events.DESCRIPTION, args["description"] as String)
put(Events.DTSTART, start); put(Events.DTEND, end)
put(Events.EVENT_TIMEZONE, "Asia/Shanghai")
put(Events.CUSTOM_APP_PACKAGE, owner); put(Events.CUSTOM_APP_URI, marker(key))
}
val id = args["eventId"] as? String
if (id != null && ownership(id, key, calendarId)) {
val count = resolver.update(ContentUris.withAppendedId(Events.CONTENT_URI,id.toLong()), values,
"${Events.CUSTOM_APP_PACKAGE}=? AND ${Events.CUSTOM_APP_URI}=? AND ${Events.CALENDAR_ID}=?",
arrayOf(owner, marker(key), calendarId))
check(count==1) { "日程已被修改" }
return id
}
return ContentUris.parseId(resolver.insert(Events.CONTENT_URI, values) ?: error("日程写入失败")).toString()
}
private fun delete(id: String, key: String) {
if (!ownership(id,key)) return
resolver.delete(ContentUris.withAppendedId(Events.CONTENT_URI,id.toLong()),
"${Events.CUSTOM_APP_PACKAGE}=? AND ${Events.CUSTOM_APP_URI}=?", arrayOf(owner,marker(key)))
}
}
@@ -1,5 +1,20 @@
package com.example.hepan_everyday
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {
private var calendar: CalendarBridge? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
calendar = CalendarBridge(this, flutterEngine.dartExecutor.binaryMessenger)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
calendar?.onPermissionResult(requestCode)
}
override fun onDestroy() {
calendar?.close()
super.onDestroy()
}
}
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M7,2h2v2h6V2h2v2h2a2,2 0,0 1,2 2v14a2,2 0,0 1,-2 2H5a2,2 0,0 1,-2 -2V6a2,2 0,0 1,2 -2h2zM5,10v10h14V10zM7,12h4v4H7z"/>
</vector>