11//
22// Calendar Notifications Plus
33// Copyright (C) 2016 Sergey Parshin (s.parshin.sc@gmail.com)
4+ // Copyright (C) 2025 William Harris (wharris+cnplus@upscalews.com)
45//
56// This program is free software; you can redistribute it and/or modify
67// it under the terms of the GNU General Public License as published by
1819//
1920package com.github.quarck.calnotify.prefs
2021
22+ import android.content.ActivityNotFoundException
23+ import android.content.ContentResolver
2124import android.content.Context
25+ import android.content.Intent
2226import android.graphics.drawable.ColorDrawable
2327import android.os.Bundle
24- import androidx.appcompat.app.AppCompatActivity
25- import androidx.recyclerview.widget.RecyclerView
26- import androidx.recyclerview.widget.StaggeredGridLayoutManager
27- import androidx.appcompat.widget.Toolbar
28+ import android.provider.CalendarContract
29+ import android.provider.Settings
2830import android.view.LayoutInflater
31+ import android.view.Menu
32+ import android.view.MenuItem
2933import android.view.View
3034import android.view.ViewGroup
3135import android.widget.CheckBox
3236import android.widget.LinearLayout
3337import android.widget.TextView
38+ import androidx.appcompat.app.AlertDialog
39+ import androidx.appcompat.app.AppCompatActivity
40+ import androidx.appcompat.widget.Toolbar
41+ import androidx.recyclerview.widget.RecyclerView
42+ import androidx.recyclerview.widget.StaggeredGridLayoutManager
43+ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
3444import com.github.quarck.calnotify.Consts
3545import com.github.quarck.calnotify.R
36- import com.github.quarck.calnotify.Settings
46+ import com.github.quarck.calnotify.Settings as AppSettings
3747import com.github.quarck.calnotify.calendar.CalendarProvider
3848import com.github.quarck.calnotify.calendar.CalendarRecord
3949import com.github.quarck.calnotify.logs.DevLog
40- // import com.github.quarck.calnotify.logs.Logger
4150import com.github.quarck.calnotify.utils.background
4251import com.github.quarck.calnotify.utils.find
4352import com.github.quarck.calnotify.utils.findOrThrow
@@ -48,7 +57,8 @@ class CalendarListEntry(
4857 val type : CalendarListEntryType ,
4958 val headerTitle : String? = null ,
5059 val calendar : CalendarRecord ? = null ,
51- var isHandled : Boolean = true
60+ var isHandled : Boolean = true ,
61+ val upcomingEventCount : Int = 0
5262)
5363
5464
@@ -113,7 +123,14 @@ class CalendarListAdapter(val context: Context, var entries: Array<CalendarListE
113123 }
114124
115125 CalendarListEntryType .Calendar -> {
116- holder.checkboxCalendarName.text = entry.calendar?.name ? : " "
126+ val calendarName = entry.calendar?.name ? : " "
127+ // Show upcoming event count for unhandled calendars with events
128+ val displayText = if (! entry.isHandled && entry.upcomingEventCount > 0 ) {
129+ context.getString(R .string.calendar_name_with_event_count, calendarName, entry.upcomingEventCount)
130+ } else {
131+ calendarName
132+ }
133+ holder.checkboxCalendarName.text = displayText
117134 holder.calendarAccountName.visibility = View .GONE
118135 holder.calendarEntryLayout.visibility = View .VISIBLE
119136 holder.colorView.background = ColorDrawable (entry.calendar?.color ? : Consts .DEFAULT_CALENDAR_EVENT_COLOR )
@@ -142,8 +159,9 @@ class CalendarsActivity : AppCompatActivity() {
142159 private lateinit var staggeredLayoutManager: StaggeredGridLayoutManager
143160 private lateinit var recyclerView: RecyclerView
144161 private lateinit var noCalendarsText: TextView
162+ private lateinit var swipeRefreshLayout: SwipeRefreshLayout
145163
146- private lateinit var settings: Settings
164+ private lateinit var settings: AppSettings
147165
148166 override fun onCreate (savedInstanceState : Bundle ? ) {
149167 super .onCreate(savedInstanceState)
@@ -155,7 +173,7 @@ class CalendarsActivity : AppCompatActivity() {
155173 supportActionBar?.setDisplayHomeAsUpEnabled(true )
156174 supportActionBar?.setDisplayShowHomeEnabled(true )
157175
158- settings = Settings (this )
176+ settings = AppSettings (this )
159177
160178 adapter = CalendarListAdapter (this , arrayOf<CalendarListEntry >())
161179
@@ -172,53 +190,126 @@ class CalendarsActivity : AppCompatActivity() {
172190 recyclerView.adapter = adapter;
173191
174192 noCalendarsText = findOrThrow<TextView >(R .id.no_calendars_text)
193+
194+ swipeRefreshLayout = findOrThrow<SwipeRefreshLayout >(R .id.swipe_refresh_calendars)
195+ swipeRefreshLayout.setOnRefreshListener {
196+ requestCalendarSyncAndRefresh()
197+ }
175198 }
176199
177- override fun onResume () {
178- super .onResume()
200+ override fun onCreateOptionsMenu (menu : Menu ): Boolean {
201+ menuInflater.inflate(R .menu.menu_calendars, menu)
202+ return true
203+ }
179204
180- background {
181- // load the data here
182- val calendars = CalendarProvider .getCalendars(this ).toTypedArray()
183-
184- val entries = mutableListOf<CalendarListEntry >()
185-
186- // Arrange entries by accountName calendar
187- for ((accountName, type) in calendars.map { Pair (it.accountName, it.accountType) }.toSet()) {
188-
189- // Add group title
190- entries.add(CalendarListEntry (type = CalendarListEntryType .Header , headerTitle = accountName))
191-
192- // Add all the calendars for this accountName
193- entries.addAll(
194- calendars
195- .filter { it.accountName == accountName && it.accountType == type }
196- .sortedBy { it.calendarId }
197- .map {
198- CalendarListEntry (
199- type = CalendarListEntryType .Calendar ,
200- calendar = it,
201- isHandled = settings.getCalendarIsHandled(it.calendarId))
202- })
203-
204- // Add a divider
205- entries.add(CalendarListEntry (type = CalendarListEntryType .Divider ))
205+ override fun onOptionsItemSelected (item : MenuItem ): Boolean {
206+ return when (item.itemId) {
207+ R .id.action_refresh_calendars -> {
208+ swipeRefreshLayout.isRefreshing = true
209+ requestCalendarSyncAndRefresh()
210+ true
211+ }
212+ R .id.action_calendar_sync_help -> {
213+ showCalendarSyncHelp()
214+ true
206215 }
216+ else -> super .onOptionsItemSelected(item)
217+ }
218+ }
207219
208- // remove last divider
209- if (entries.size >= 1 && entries[entries.size - 1 ].type == CalendarListEntryType .Divider )
210- entries.removeAt(entries.size - 1 )
220+ private fun requestCalendarSyncAndRefresh () {
221+ background {
222+ try {
223+ val extras = Bundle ().apply {
224+ putBoolean(ContentResolver .SYNC_EXTRAS_MANUAL , true )
225+ putBoolean(ContentResolver .SYNC_EXTRAS_EXPEDITED , true )
226+ }
227+ ContentResolver .requestSync(null , CalendarContract .AUTHORITY , extras)
228+ DevLog .info(LOG_TAG , " Requested calendar sync" )
211229
212- val entriesFinal = entries.toTypedArray()
230+ // Wait for sync to start/complete
231+ Thread .sleep(SYNC_WAIT_MS )
232+ } catch (ex: SecurityException ) {
233+ DevLog .error(LOG_TAG , " SecurityException requesting sync: ${ex.message} " )
234+ }
213235
214- runOnUiThread {
215- // update activity finally
236+ loadCalendars()
216237
217- noCalendarsText.visibility = if (entriesFinal.isNotEmpty()) View .GONE else View .VISIBLE
238+ runOnUiThread {
239+ swipeRefreshLayout.isRefreshing = false
240+ }
241+ }
242+ }
218243
219- adapter.entries = entriesFinal
220- adapter.notifyDataSetChanged();
244+ private fun showCalendarSyncHelp () {
245+ AlertDialog .Builder (this )
246+ .setTitle(R .string.calendar_sync_help_title)
247+ .setMessage(R .string.calendar_sync_help_message)
248+ .setPositiveButton(android.R .string.ok, null )
249+ .setNeutralButton(R .string.open_sync_settings) { _, _ ->
250+ openSyncSettings()
221251 }
252+ .show()
253+ }
254+
255+ private fun openSyncSettings () {
256+ try {
257+ val intent = Intent (Settings .ACTION_SYNC_SETTINGS )
258+ startActivity(intent)
259+ } catch (ex: ActivityNotFoundException ) {
260+ DevLog .error(LOG_TAG , " Could not open sync settings: ${ex.message} " )
261+ startActivity(Intent (Settings .ACTION_SETTINGS ))
262+ }
263+ }
264+
265+ private fun loadCalendars () {
266+ val calendars = CalendarProvider .getCalendars(this ).toTypedArray()
267+ val upcomingCounts = CalendarProvider .getUpcomingEventCountsByCalendar(this , UPCOMING_EVENTS_DAYS )
268+
269+ val entries = mutableListOf<CalendarListEntry >()
270+
271+ // Arrange entries by accountName calendar
272+ for ((accountName, type) in calendars.map { Pair (it.accountName, it.accountType) }.toSet()) {
273+
274+ // Add group title
275+ entries.add(CalendarListEntry (type = CalendarListEntryType .Header , headerTitle = accountName))
276+
277+ // Add all the calendars for this accountName
278+ entries.addAll(
279+ calendars
280+ .filter { it.accountName == accountName && it.accountType == type }
281+ .sortedBy { it.calendarId }
282+ .map {
283+ CalendarListEntry (
284+ type = CalendarListEntryType .Calendar ,
285+ calendar = it,
286+ isHandled = settings.getCalendarIsHandled(it.calendarId),
287+ upcomingEventCount = upcomingCounts[it.calendarId] ? : 0 )
288+ })
289+
290+ // Add a divider
291+ entries.add(CalendarListEntry (type = CalendarListEntryType .Divider ))
292+ }
293+
294+ // remove last divider
295+ if (entries.size >= 1 && entries[entries.size - 1 ].type == CalendarListEntryType .Divider )
296+ entries.removeAt(entries.size - 1 )
297+
298+ val entriesFinal = entries.toTypedArray()
299+
300+ runOnUiThread {
301+ noCalendarsText.visibility = if (entriesFinal.isNotEmpty()) View .GONE else View .VISIBLE
302+
303+ adapter.entries = entriesFinal
304+ adapter.notifyDataSetChanged();
305+ }
306+ }
307+
308+ override fun onResume () {
309+ super .onResume()
310+
311+ background {
312+ loadCalendars()
222313 }
223314 }
224315
@@ -229,5 +320,7 @@ class CalendarsActivity : AppCompatActivity() {
229320
230321 companion object {
231322 private const val LOG_TAG = " CalendarsActivity"
323+ private const val SYNC_WAIT_MS = 2000L
324+ private const val UPCOMING_EVENTS_DAYS = 7
232325 }
233326}
0 commit comments