Skip to content

Commit a1a7a9d

Browse files
Calendar Manager (#92)
* CalendarManager Added a basic implementation to manipulate the user's default Calendar. At the moment we can only add and list the events create with the app, but with this base it is easy to add the functions for removing and updating events. Same with Reminders. Also: - Added an example of LayoutAnimation for animation the insertion of Items in a RecyclerView. - Move adapters package inside the ui package. - Upgrade Kotlin version from 4.1.2 to 4.1.3. - Enabled desugaring for JAVA 8+ API supporting
1 parent 6262620 commit a1a7a9d

32 files changed

+689
-15
lines changed

app/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ android {
9999
compileOptions {
100100
sourceCompatibility JavaVersion.VERSION_1_8
101101
targetCompatibility JavaVersion.VERSION_1_8
102+
// Flag to enable support for the new language APIs
103+
coreLibraryDesugaringEnabled true
102104
}
103105
kotlinOptions {
104106
jvmTarget = '1.8'
@@ -174,6 +176,9 @@ dependencies {
174176
implementation "androidx.fragment:fragment-ktx:$ktx_fragment_version"
175177
implementation "androidx.core:core-ktx:$core_version"
176178

179+
// Java 8+ API desugaring support
180+
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
181+
177182
// Code style
178183
ktlint "com.pinterest:ktlint:$ktlint_version"
179184

app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
1414
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
1515

16+
<!--Calendar-->
17+
<uses-permission android:name="android.permission.READ_CALENDAR" />
18+
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
19+
1620
<uses-feature
1721
android:name="android.hardware.camera"
1822
android:required="false" />

app/src/main/java/com/rocketinsights/android/RocketApplication.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ class RocketApplication : Application() {
3737
}
3838

3939
private fun init() {
40-
4140
if (BuildConfig.DEBUG) {
4241
Timber.plant(Timber.DebugTree())
4342
}

app/src/main/java/com/rocketinsights/android/di/KoinUtils.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import com.rocketinsights.android.auth.SessionWatcher
1515
import com.rocketinsights.android.coroutines.DispatcherProvider
1616
import com.rocketinsights.android.coroutines.DispatcherProviderImpl
1717
import com.rocketinsights.android.db.Database
18+
import com.rocketinsights.android.managers.CalendarManager
19+
import com.rocketinsights.android.managers.CalendarManagerImpl
1820
import com.rocketinsights.android.managers.InternetManager
1921
import com.rocketinsights.android.managers.PermissionsManager
2022
import com.rocketinsights.android.managers.PermissionsManagerImpl
@@ -31,6 +33,7 @@ import com.rocketinsights.android.repos.AuthRepository
3133
import com.rocketinsights.android.repos.MessageRepository
3234
import com.rocketinsights.android.ui.MainActivity
3335
import com.rocketinsights.android.ui.ParentScrollProvider
36+
import com.rocketinsights.android.viewmodels.CalendarViewModel
3437
import com.rocketinsights.android.viewmodels.ConnectivityViewModel
3538
import com.rocketinsights.android.viewmodels.LocationViewModel
3639
import com.rocketinsights.android.viewmodels.MainViewModel
@@ -112,6 +115,7 @@ private fun managersModule() = module {
112115
)
113116
}
114117
single<LocalStore> { LocalStoreImpl(get()) }
118+
single<CalendarManager> { CalendarManagerImpl(get(), get(), get()) }
115119
}
116120

117121
private fun repositoryModule() = module {
@@ -146,6 +150,7 @@ private fun viewModelsModule() = module {
146150
viewModel { PermissionsViewModel(get()) }
147151
viewModel { PhotoViewModel() }
148152
viewModel { LocationViewModel(get(), get()) }
153+
viewModel { CalendarViewModel(get()) }
149154
}
150155

151156
private fun viewInteractorsModule() = module {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.rocketinsights.android.extensions
2+
3+
import com.rocketinsights.android.databinding.LayoutProgressBinding
4+
5+
fun LayoutProgressBinding.show() {
6+
root.show()
7+
}
8+
9+
fun LayoutProgressBinding.hide() {
10+
root.hide()
11+
}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package com.rocketinsights.android.managers
2+
3+
import android.Manifest
4+
import android.content.ContentUris
5+
import android.content.ContentValues
6+
import android.content.Context
7+
import android.database.Cursor
8+
import android.net.Uri
9+
import android.provider.CalendarContract
10+
import com.rocketinsights.android.coroutines.DispatcherProvider
11+
import com.rocketinsights.android.prefs.LocalStore
12+
import kotlinx.coroutines.flow.Flow
13+
import kotlinx.coroutines.flow.MutableStateFlow
14+
import kotlinx.coroutines.flow.firstOrNull
15+
import kotlinx.coroutines.withContext
16+
import timber.log.Timber
17+
import java.text.SimpleDateFormat
18+
import java.time.Year
19+
import java.time.ZonedDateTime
20+
import java.util.Calendar
21+
22+
/**
23+
* CalendarManager allows us to add events to the current user's default Calendar through a ContentProvider.
24+
*
25+
* There are several things to keep in mind:
26+
* - In obtaining the events, it is necessary to pass a range of dates, which is now hardcoded a
27+
* range between two years less and more than the current one.
28+
* - There is another simpler way to manipulate the calendar through Intents, but these make the user
29+
* leave the current application to end the action in the device's calendar app. In addition, if we
30+
* use Intents it is not necessary to request read and write permissions for the calendar.
31+
*
32+
* More information about Calendar Provider:
33+
* https://developer.android.com/guide/topics/providers/calendar-provider
34+
*/
35+
interface CalendarManager {
36+
companion object {
37+
val CALENDAR_PERMISSIONS = arrayOf(
38+
Manifest.permission.READ_CALENDAR,
39+
Manifest.permission.WRITE_CALENDAR
40+
)
41+
}
42+
43+
suspend fun addEvent(
44+
title: String,
45+
description: String,
46+
startDate: ZonedDateTime,
47+
endDate: ZonedDateTime,
48+
address: String
49+
): Long
50+
51+
fun events(): Flow<List<CalendarEvent>>
52+
53+
suspend fun refreshEvents()
54+
}
55+
56+
data class CalendarEvent(val id: Long, val title: String, val description: String)
57+
58+
class CalendarManagerImpl(
59+
private val context: Context,
60+
private val localStore: LocalStore,
61+
private val dispatcher: DispatcherProvider
62+
) : CalendarManager {
63+
private val savedEventsIds = localStore.getStringSetValue(CALENDAR_EVENTS_IDS)
64+
65+
private val events = MutableStateFlow<List<CalendarEvent>>(listOf())
66+
67+
companion object {
68+
const val CALENDAR_EVENTS_IDS = "CALENDAR_EVENTS_IDS"
69+
}
70+
71+
override suspend fun addEvent(
72+
title: String,
73+
description: String,
74+
startDate: ZonedDateTime,
75+
endDate: ZonedDateTime,
76+
address: String
77+
): Long = withContext(dispatcher.io()) {
78+
val startMillis = startDate.toInstant().toEpochMilli()
79+
val endMillis = endDate.toInstant().toEpochMilli()
80+
81+
val values = ContentValues().apply {
82+
put(CalendarContract.Events.DTSTART, startMillis)
83+
put(CalendarContract.Events.DTEND, endMillis)
84+
put(CalendarContract.Events.TITLE, title)
85+
put(CalendarContract.Events.DESCRIPTION, description)
86+
put(CalendarContract.Events.CALENDAR_ID, getDefaultCalendarId())
87+
put(CalendarContract.Events.EVENT_TIMEZONE, startDate.zone.id)
88+
}
89+
90+
val uri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, values)
91+
92+
// Get the event ID that is the last element in the Uri
93+
val eventId = uri?.lastPathSegment?.toLong() ?: -1
94+
95+
if (eventId != -1L) {
96+
// Update Saved Events Info
97+
var savedEvents = (
98+
savedEventsIds.firstOrNull()?.toMutableSet()
99+
?: mutableSetOf()
100+
).apply {
101+
add(eventId.toString())
102+
}
103+
104+
localStore.setStringSetValue(CALENDAR_EVENTS_IDS, savedEvents)
105+
106+
// Refresh Events
107+
retrieveEvents()
108+
}
109+
110+
return@withContext eventId
111+
}
112+
113+
override fun events(): Flow<List<CalendarEvent>> = events
114+
115+
override suspend fun refreshEvents() = withContext(dispatcher.io()) {
116+
retrieveEvents()
117+
}
118+
119+
private suspend fun retrieveEvents() {
120+
val instanceProjection: Array<String> = arrayOf(
121+
CalendarContract.Instances.EVENT_ID, // 0
122+
CalendarContract.Instances.BEGIN, // 1
123+
CalendarContract.Instances.TITLE, // 2
124+
CalendarContract.Instances.DESCRIPTION // 3
125+
)
126+
127+
// The indices for the projection array above.
128+
val projectionIdIndex = 0
129+
val projectionBeginIndex = 1
130+
val projectionTitleIndex = 2
131+
val projectionDescriptionIndex = 3
132+
133+
// Specify the date range you want to search for recurring
134+
// event instances
135+
// TODO: Update this according to the App Requirements
136+
val currentYear = Year.now().value
137+
138+
val startMillis: Long = Calendar.getInstance().run {
139+
set(currentYear - 2, 1, 1, 0, 0)
140+
timeInMillis
141+
}
142+
val endMillis: Long = Calendar.getInstance().run {
143+
set(currentYear + 2, 1, 1, 0, 0)
144+
timeInMillis
145+
}
146+
147+
// Get Saved Events
148+
var savedEvents = savedEventsIds.firstOrNull()?.joinToString(separator = ",") ?: ""
149+
150+
val selection = "${CalendarContract.Instances.CALENDAR_ID} = ${getDefaultCalendarId()} AND " +
151+
"${CalendarContract.Instances.EVENT_ID} IN ($savedEvents)"
152+
153+
// Construct the query with the desired date range.
154+
val builder: Uri.Builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
155+
ContentUris.appendId(builder, startMillis)
156+
ContentUris.appendId(builder, endMillis)
157+
158+
// Submit the query
159+
val cur: Cursor? = context.contentResolver.query(
160+
builder.build(),
161+
instanceProjection,
162+
selection,
163+
null,
164+
null
165+
)
166+
167+
val updatedEvents = mutableListOf<CalendarEvent>()
168+
169+
while (cur != null && cur.moveToNext()) {
170+
// Get the field values
171+
val id: Long = cur.getLong(projectionIdIndex)
172+
val beginVal: Long = cur.getLong(projectionBeginIndex)
173+
val title: String = cur.getString(projectionTitleIndex)
174+
val description: String = cur.getString(projectionDescriptionIndex)
175+
176+
val calendar = Calendar.getInstance().apply {
177+
timeInMillis = beginVal
178+
}
179+
val formatter = SimpleDateFormat("MM/dd/yyyy")
180+
Timber.d("Calendar Event Found with Date: ${formatter.format(calendar.time)}")
181+
182+
updatedEvents.add(CalendarEvent(id = id, title = title, description = description))
183+
}
184+
185+
events.emit(updatedEvents)
186+
}
187+
188+
private fun getDefaultCalendarId(): Long? {
189+
val projection = arrayOf(CalendarContract.Calendars._ID, CalendarContract.Calendars.CALENDAR_DISPLAY_NAME)
190+
191+
var calendarCursor = context.contentResolver.query(
192+
CalendarContract.Calendars.CONTENT_URI,
193+
projection,
194+
CalendarContract.Calendars.VISIBLE + " = 1 AND " + CalendarContract.Calendars.IS_PRIMARY + " = 1",
195+
null,
196+
CalendarContract.Calendars._ID + " ASC"
197+
)
198+
199+
if (calendarCursor != null && calendarCursor.count <= 0) {
200+
calendarCursor = context.contentResolver.query(
201+
CalendarContract.Calendars.CONTENT_URI,
202+
projection,
203+
CalendarContract.Calendars.VISIBLE + " = 1",
204+
null,
205+
CalendarContract.Calendars._ID + " ASC"
206+
)
207+
}
208+
209+
return if (calendarCursor != null &&
210+
calendarCursor.moveToFirst()
211+
) {
212+
val calendarName: String
213+
val calendarID: String
214+
val nameCol = calendarCursor.getColumnIndex(projection[1])
215+
val idCol = calendarCursor.getColumnIndex(projection[0])
216+
217+
calendarName = calendarCursor.getString(nameCol)
218+
calendarID = calendarCursor.getString(idCol)
219+
220+
calendarCursor.close()
221+
222+
calendarID.toLong()
223+
} else {
224+
null
225+
}
226+
}
227+
}

0 commit comments

Comments
 (0)