fix: open existing events from external VIEW intents (Codeberg #48) #69
@@ -123,6 +123,22 @@
|
|||||||
<data android:mimeType="vnd.android.cursor.dir/event" />
|
<data android:mimeType="vnd.android.cursor.dir/event" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Open an existing event another app/widget points at (e.g. tapping
|
||||||
|
an event in the Todo Agenda widget): ACTION_VIEW on
|
||||||
|
content://com.android.calendar/events/<id>, the way AOSP fires it.
|
||||||
|
Matched by the provider's item MIME type, not the path — a
|
||||||
|
content: VIEW intent carries the resolved type
|
||||||
|
(vnd.android.cursor.item/event) and a path-only filter wouldn't
|
||||||
|
match it. The occurrence's times ride as EXTRA_EVENT_BEGIN_TIME /
|
||||||
|
EXTRA_EVENT_END_TIME when the launcher supplies them
|
||||||
|
(MainActivity.viewEventKeyOrNull, issue #48). -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:mimeType="vnd.android.cursor.item/event" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Launcher long-press shortcuts (e.g. "New event"). -->
|
<!-- Launcher long-press shortcuts (e.g. "New event"). -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
||||||
import de.jeanlucmakiola.calendula.ui.WidgetNavRequest
|
import de.jeanlucmakiola.calendula.ui.WidgetNavRequest
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
|
import de.jeanlucmakiola.calendula.ui.detail.EventDetailViewModel.Companion.NO_OCCURRENCE_TIME
|
||||||
import de.jeanlucmakiola.floret.components.DebugRibbon
|
import de.jeanlucmakiola.floret.components.DebugRibbon
|
||||||
import de.jeanlucmakiola.calendula.ui.crash.CrashReportActivity
|
import de.jeanlucmakiola.calendula.ui.crash.CrashReportActivity
|
||||||
import de.jeanlucmakiola.calendula.domain.FontRole
|
import de.jeanlucmakiola.calendula.domain.FontRole
|
||||||
@@ -90,7 +91,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
requestedDetailKey = intent.detailKeyOrNull()
|
requestedDetailKey = intent.detailKeyOrNull() ?: intent.viewEventKeyOrNull()
|
||||||
requestedNav = intent.navRequestOrNull()
|
requestedNav = intent.navRequestOrNull()
|
||||||
requestedImportUri = intent.importUriOrNull()
|
requestedImportUri = intent.importUriOrNull()
|
||||||
requestedInsertForm = intent.insertFormOrNull()
|
requestedInsertForm = intent.insertFormOrNull()
|
||||||
@@ -178,7 +179,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
intent.detailKeyOrNull()?.let { requestedDetailKey = it }
|
(intent.detailKeyOrNull() ?: intent.viewEventKeyOrNull())?.let { requestedDetailKey = it }
|
||||||
intent.navRequestOrNull()?.let { requestedNav = it }
|
intent.navRequestOrNull()?.let { requestedNav = it }
|
||||||
intent.importUriOrNull()?.let { requestedImportUri = it }
|
intent.importUriOrNull()?.let { requestedImportUri = it }
|
||||||
intent.insertFormOrNull()?.let { requestedInsertForm = it }
|
intent.insertFormOrNull()?.let { requestedInsertForm = it }
|
||||||
@@ -293,6 +294,30 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The detail key for an external "open this event" — ACTION_VIEW on
|
||||||
|
* `content://com.android.calendar/events/<id>`, the way AOSP fires it (e.g.
|
||||||
|
* tapping an existing event in the Todo Agenda widget, issue #48). Reuses the
|
||||||
|
* same occurrence-key channel as reminder taps. The launcher passes the
|
||||||
|
* occurrence's times as `EXTRA_EVENT_BEGIN_TIME` / `EXTRA_EVENT_END_TIME` when
|
||||||
|
* it has them; a bare URI omits them, so we carry [NO_OCCURRENCE_TIME] and
|
||||||
|
* [EventDetailViewModel] falls back to the event row's own DTSTART/DTEND
|
||||||
|
* rather than rendering at the epoch.
|
||||||
|
*/
|
||||||
|
private fun Intent.viewEventKeyOrNull(): LongArray? {
|
||||||
|
if (action != Intent.ACTION_VIEW) return null
|
||||||
|
val uri = data ?: return null
|
||||||
|
if (uri.host != CALENDAR_PROVIDER_HOST) return null
|
||||||
|
val segments = uri.pathSegments
|
||||||
|
if (segments.firstOrNull() != "events") return null
|
||||||
|
val eventId = segments.getOrNull(1)?.toLongOrNull() ?: return null
|
||||||
|
return longArrayOf(
|
||||||
|
eventId,
|
||||||
|
longExtraOrNull(CalendarContract.EXTRA_EVENT_BEGIN_TIME) ?: NO_OCCURRENCE_TIME,
|
||||||
|
longExtraOrNull(CalendarContract.EXTRA_EVENT_END_TIME) ?: NO_OCCURRENCE_TIME,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// The calendar provider's authority/host. A date tap arrives as
|
// The calendar provider's authority/host. A date tap arrives as
|
||||||
// ACTION_VIEW on content://com.android.calendar/time/<epochMillis>.
|
// ACTION_VIEW on content://com.android.calendar/time/<epochMillis>.
|
||||||
|
|||||||
@@ -141,13 +141,20 @@ class EventDetailViewModel @Inject constructor(
|
|||||||
private suspend fun loadDetail(target: Target): EventDetailUiState = try {
|
private suspend fun loadDetail(target: Target): EventDetailUiState = try {
|
||||||
val detail = repository.eventDetail(target.eventId)
|
val detail = repository.eventDetail(target.eventId)
|
||||||
// The Events row holds the series start; replace it with this
|
// The Events row holds the series start; replace it with this
|
||||||
// occurrence's time so recurring events render correctly.
|
// occurrence's time so recurring events render correctly. An external
|
||||||
val corrected = detail.copy(
|
// "open event" that names no occurrence ([NO_OCCURRENCE_TIME] — e.g. a
|
||||||
instance = detail.instance.copy(
|
// bare content://.../events/<id> VIEW intent, issue #48) keeps the row's
|
||||||
start = Instant.fromEpochMilliseconds(target.beginMillis),
|
// own DTSTART/DTEND instead of overriding it to the epoch.
|
||||||
end = Instant.fromEpochMilliseconds(target.endMillis),
|
val corrected = if (target.beginMillis == NO_OCCURRENCE_TIME) {
|
||||||
),
|
detail
|
||||||
)
|
} else {
|
||||||
|
detail.copy(
|
||||||
|
instance = detail.instance.copy(
|
||||||
|
start = Instant.fromEpochMilliseconds(target.beginMillis),
|
||||||
|
end = Instant.fromEpochMilliseconds(target.endMillis),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
val calendar = repository.calendars().first()
|
val calendar = repository.calendars().first()
|
||||||
.firstOrNull { it.id == corrected.instance.calendarId }
|
.firstOrNull { it.id == corrected.instance.calendarId }
|
||||||
EventDetailUiState.Success(
|
EventDetailUiState.Success(
|
||||||
@@ -168,6 +175,16 @@ class EventDetailViewModel @Inject constructor(
|
|||||||
|
|
||||||
/** A tapped occurrence: the series [eventId] plus this occurrence's own times. */
|
/** A tapped occurrence: the series [eventId] plus this occurrence's own times. */
|
||||||
private data class Target(val eventId: Long, val beginMillis: Long, val endMillis: Long)
|
private data class Target(val eventId: Long, val beginMillis: Long, val endMillis: Long)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Sentinel begin/end for an "open this event" that names no occurrence —
|
||||||
|
* a bare `content://com.android.calendar/events/<id>` VIEW intent with no
|
||||||
|
* `EXTRA_EVENT_BEGIN_TIME` (issue #48). [loadDetail] then keeps the event
|
||||||
|
* row's own DTSTART/DTEND instead of overriding it to the epoch.
|
||||||
|
*/
|
||||||
|
const val NO_OCCURRENCE_TIME: Long = Long.MIN_VALUE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A filesystem-safe `.ics` file name from an event title (or a fallback). */
|
/** A filesystem-safe `.ics` file name from an event title (or a fallback). */
|
||||||
|
|||||||
Reference in New Issue
Block a user