feat: add local reminders and owned calendar synchronization
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user