Skip to content

Commit ac29dab

Browse files
committed
{shared|androidApp}: Support for session bookmark and alarms
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
1 parent 760392f commit ac29dab

File tree

15 files changed

+378
-14
lines changed

15 files changed

+378
-14
lines changed

androidApp/src/main/AndroidManifest.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<!--
3-
SPDX-FileCopyrightText: 2024 OPass
3+
SPDX-FileCopyrightText: 2024-2025 OPass
44
SPDX-License-Identifier: GPL-3.0-only
55
-->
66
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
@@ -19,6 +19,8 @@
1919
<uses-permission android:name="android.permission.INTERNET" />
2020
<uses-permission android:name="android.permission.CAMERA" />
2121
<uses-permission android:name="android.permission.VIBRATE"/>
22+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
23+
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
2224

2325
<application
2426
android:name=".OPassApp"
@@ -46,5 +48,9 @@
4648
android:name="autoStoreLocales"
4749
android:value="true" />
4850
</service>
51+
52+
<receiver
53+
android:name=".receiver.SessionAlarmReceiver"
54+
android:exported="false" />
4955
</application>
5056
</manifest>

androidApp/src/main/java/app/opass/ccip/android/OPassApp.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,18 @@
66
package app.opass.ccip.android
77

88
import android.app.Application
9+
import android.os.Build
10+
import app.opass.ccip.android.utils.NotificationUtil
911
import dagger.hilt.android.HiltAndroidApp
1012

1113
@HiltAndroidApp
12-
class OPassApp: Application()
14+
class OPassApp : Application() {
15+
16+
override fun onCreate() {
17+
super.onCreate()
18+
19+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
20+
NotificationUtil.createNotificationChannels(this)
21+
}
22+
}
23+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 OPass
3+
* SPDX-License-Identifier: GPL-3.0-only
4+
*/
5+
6+
package app.opass.ccip.android.receiver
7+
8+
import android.app.NotificationManager
9+
import android.content.BroadcastReceiver
10+
import android.content.Context
11+
import android.content.Intent
12+
import androidx.core.content.getSystemService
13+
import app.opass.ccip.android.utils.AlarmUtil.INTENT_ACTION_SESSION_ALARM
14+
import app.opass.ccip.android.utils.AlarmUtil.INTENT_EXTRA_EVENT_ID
15+
import app.opass.ccip.android.utils.AlarmUtil.INTENT_EXTRA_SESSION_ID
16+
import app.opass.ccip.android.utils.AlarmUtil.INTENT_EXTRA_SESSION_ROOM
17+
import app.opass.ccip.android.utils.AlarmUtil.INTENT_EXTRA_SESSION_TITLE
18+
import app.opass.ccip.android.utils.NotificationUtil
19+
20+
class SessionAlarmReceiver: BroadcastReceiver() {
21+
22+
override fun onReceive(context: Context?, intent: Intent?) {
23+
if (context == null || intent?.action != INTENT_ACTION_SESSION_ALARM) return
24+
25+
val eventId = intent.getStringExtra(INTENT_EXTRA_EVENT_ID)!!
26+
val sessionId = intent.getStringExtra(INTENT_EXTRA_SESSION_ID)!!
27+
val title = intent.getStringExtra(INTENT_EXTRA_SESSION_TITLE)!!
28+
val room = intent.getStringExtra(INTENT_EXTRA_SESSION_ROOM)!!
29+
30+
val notificationManager = context.getSystemService<NotificationManager>()!!
31+
notificationManager.notify(
32+
"${eventId}_${sessionId}".hashCode(),
33+
NotificationUtil.getSessionNotification(context, eventId, sessionId, title, room)
34+
)
35+
}
36+
}

androidApp/src/main/java/app/opass/ccip/android/ui/extensions/SharedPreferences.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import androidx.core.content.edit
1111
private const val CURRENT_EVENT_ID = "CURRENT_EVENT_ID"
1212
private const val TOKEN = "TOKEN"
1313
private const val AUTO_BRIGHTEN = "AUTO_BRIGHTEN"
14+
private const val BOOKMARK = "BOOKMARK"
15+
private const val ALARMS = "ALARMS"
1416

1517
val SharedPreferences.autoBrighten: Boolean
1618
get() = this.getBoolean(AUTO_BRIGHTEN, true)
@@ -37,3 +39,19 @@ fun SharedPreferences.removeToken(eventId: String) {
3739
fun SharedPreferences.autoBrighten(enabled: Boolean) {
3840
return this.edit { putBoolean(AUTO_BRIGHTEN, enabled) }
3941
}
42+
43+
fun SharedPreferences.getBookmarks(eventId: String): Set<String> {
44+
return this.getStringSet("${eventId}_$BOOKMARK", emptySet()) ?: emptySet()
45+
}
46+
47+
fun SharedPreferences.saveBookmarks(eventId: String, sessionIDs: Set<String>) {
48+
return this.edit { putStringSet("${eventId}_$BOOKMARK", sessionIDs) }
49+
}
50+
51+
fun SharedPreferences.getAlarms(eventId: String): Set<String> {
52+
return this.getStringSet("${eventId}_$ALARMS", emptySet()) ?: emptySet()
53+
}
54+
55+
fun SharedPreferences.saveAlarms(eventId: String, sessionIDs: Set<String>) {
56+
return this.edit { putStringSet("${eventId}_$ALARMS", sessionIDs) }
57+
}

androidApp/src/main/java/app/opass/ccip/android/ui/menu/SessionMenu.kt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,49 @@ import app.opass.ccip.android.ui.menu.item.SessionMenuItem
2424

2525
/**
2626
* Menu for the session screen
27+
* @param hasBookmark Whether the session has been bookmarked
28+
* @param hasAlarm Whether the session has an active alarm for notifying the user
2729
* @param onMenuItemClicked Callback when a menu item has been clicked
2830
* @see SessionMenuItem
2931
*/
3032
@Composable
31-
fun SessionMenu(onMenuItemClicked: (sessionMenuItem: SessionMenuItem) -> Unit = {}) {
33+
fun SessionMenu(
34+
hasBookmark: Boolean = false,
35+
hasAlarm: Boolean = false,
36+
onMenuItemClicked: (sessionMenuItem: SessionMenuItem) -> Unit = {}
37+
) {
3238
var expanded by remember { mutableStateOf(false) }
3339
fun onClick(sessionMenuItem: SessionMenuItem) {
3440
onMenuItemClicked(sessionMenuItem)
3541
expanded = false
3642
}
3743

44+
IconButton(onClick = { onClick(SessionMenuItem.BOOKMARK) }) {
45+
Icon(
46+
painter = painterResource(
47+
if (hasBookmark) {
48+
R.drawable.ic_bookmark_filled
49+
} else {
50+
R.drawable.ic_bookmark
51+
}
52+
),
53+
contentDescription = stringResource(R.string.session_bookmark)
54+
)
55+
}
56+
57+
IconButton(onClick = { onClick(SessionMenuItem.SET_ALARM) }) {
58+
Icon(
59+
painter = painterResource(
60+
if (hasAlarm) {
61+
R.drawable.ic_notifications_active_filled
62+
} else {
63+
R.drawable.ic_notifications_active
64+
}
65+
),
66+
contentDescription = stringResource(R.string.session_alarm)
67+
)
68+
}
69+
3870
Box {
3971
IconButton(onClick = { expanded = true }) {
4072
Icon(

androidApp/src/main/java/app/opass/ccip/android/ui/menu/item/SessionMenuItem.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ package app.opass.ccip.android.ui.menu.item
99
* Valid menu items for session screen menu
1010
*/
1111
enum class SessionMenuItem {
12+
BOOKMARK,
13+
SET_ALARM,
1214
ADD_TO_CALENDAR,
1315
SHARE
1416
}

androidApp/src/main/java/app/opass/ccip/android/ui/screens/session/SessionScreen.kt

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55

66
package app.opass.ccip.android.ui.screens.session
77

8+
import android.Manifest
89
import android.content.Intent
10+
import android.os.Build
911
import android.provider.CalendarContract
1012
import android.text.format.DateUtils
1113
import android.util.Log
14+
import androidx.activity.compose.rememberLauncherForActivityResult
15+
import androidx.activity.result.contract.ActivityResultContracts
1216
import androidx.compose.foundation.layout.Column
1317
import androidx.compose.foundation.layout.fillMaxSize
1418
import androidx.compose.foundation.layout.fillMaxWidth
@@ -50,27 +54,37 @@ fun SessionScreen(
5054
val context = LocalContext.current
5155
val session by viewModel.session.collectAsStateWithLifecycle()
5256

53-
if (session != null) {
54-
val startTime = viewModel.sdf.parse(session!!.start)!!.time
55-
val endTime = viewModel.sdf.parse(session!!.end)!!.time
57+
val notificationPermissionLauncher = rememberLauncherForActivityResult(
58+
contract = ActivityResultContracts.RequestPermission(),
59+
onResult = { isGranted -> if (isGranted) viewModel.alarm = true }
60+
)
5661

62+
if (session != null) {
5763
ScreenContent(
5864
title = session!!.title,
5965
description = session!!.description,
6066
dateTime = DateUtils.formatDateRange(
6167
context,
62-
startTime,
63-
endTime,
68+
viewModel.startTime,
69+
viewModel.endTime,
6470
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL
6571
),
72+
hasAlarm = viewModel.alarm,
73+
hasBookmark = viewModel.bookmark,
6674
sessionType = session!!.type,
6775
room = session!!.room,
6876
tags = session!!.tags,
6977
speakers = session!!.speakers,
7078
onNavigateUp = onNavigateUp,
7179
onAddToCalendar = {
7280
try {
73-
context.startActivity(getCalendarIntent(session!!, startTime, endTime))
81+
context.startActivity(
82+
getCalendarIntent(
83+
session!!,
84+
viewModel.startTime,
85+
viewModel.endTime
86+
)
87+
)
7488
} catch (exception: Exception) {
7589
Log.e(TAG, "Failed to add event to the calendar app", exception)
7690
context.toast(R.string.add_to_calendar_failed)
@@ -85,6 +99,16 @@ fun SessionScreen(
8599
Log.e(TAG, "Failed to share session", exception)
86100
context.toast(R.string.share_failed)
87101
}
102+
},
103+
onSetSessionAlarm = { setAlarm ->
104+
if (setAlarm && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
105+
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
106+
} else {
107+
viewModel.alarm = setAlarm
108+
}
109+
},
110+
onSessionBookmark = { bookmark ->
111+
viewModel.bookmark = bookmark
88112
}
89113
)
90114
}
@@ -95,13 +119,17 @@ private fun ScreenContent(
95119
title: String,
96120
description: String,
97121
dateTime: String,
122+
hasBookmark: Boolean = false,
123+
hasAlarm: Boolean = false,
98124
sessionType: String? = null,
99125
room: String? = null,
100126
tags: List<String>? = emptyList(),
101127
speakers: List<String> = emptyList(),
102128
onNavigateUp: () -> Unit = {},
103129
onAddToCalendar: () -> Unit = {},
104-
onShareSession: () -> Unit = {}
130+
onShareSession: () -> Unit = {},
131+
onSessionBookmark: (bookmark: Boolean) -> Unit = {},
132+
onSetSessionAlarm: (setAlarm: Boolean) -> Unit = {}
105133
) {
106134
val context = LocalContext.current
107135
val uriHandler = object : UriHandler {
@@ -112,8 +140,10 @@ private fun ScreenContent(
112140

113141
@Composable
114142
fun SetupMenu() {
115-
SessionMenu { sessionMenuItem ->
143+
SessionMenu(hasBookmark = hasBookmark, hasAlarm = hasAlarm) { sessionMenuItem ->
116144
when (sessionMenuItem) {
145+
SessionMenuItem.BOOKMARK -> onSessionBookmark(!hasBookmark)
146+
SessionMenuItem.SET_ALARM -> onSetSessionAlarm(!hasAlarm)
117147
SessionMenuItem.SHARE -> onShareSession()
118148
SessionMenuItem.ADD_TO_CALENDAR -> onAddToCalendar()
119149
}

androidApp/src/main/java/app/opass/ccip/android/ui/screens/session/SessionViewModel.kt

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,35 @@
55

66
package app.opass.ccip.android.ui.screens.session
77

8+
import android.content.Context
89
import androidx.lifecycle.ViewModel
910
import androidx.lifecycle.viewModelScope
11+
import app.opass.ccip.android.ui.extensions.getAlarms
12+
import app.opass.ccip.android.ui.extensions.getBookmarks
13+
import app.opass.ccip.android.ui.extensions.saveAlarms
14+
import app.opass.ccip.android.ui.extensions.saveBookmarks
15+
import app.opass.ccip.android.ui.extensions.sharedPreferences
16+
import app.opass.ccip.android.utils.AlarmUtil
1017
import app.opass.ccip.helpers.PortalHelper
1118
import app.opass.ccip.network.models.schedule.Session
1219
import dagger.assisted.Assisted
1320
import dagger.assisted.AssistedFactory
1421
import dagger.assisted.AssistedInject
1522
import dagger.hilt.android.lifecycle.HiltViewModel
23+
import dagger.hilt.android.qualifiers.ApplicationContext
1624
import kotlinx.coroutines.flow.MutableStateFlow
1725
import kotlinx.coroutines.flow.asStateFlow
1826
import kotlinx.coroutines.launch
1927
import java.text.SimpleDateFormat
2028

2129
@HiltViewModel(assistedFactory = SessionViewModel.Factory::class)
2230
class SessionViewModel @AssistedInject constructor(
23-
val sdf: SimpleDateFormat,
31+
private val sdf: SimpleDateFormat,
2432
@Assisted("eventId") private val eventId: String,
2533
@Assisted("sessionId") private val sessionId: String,
26-
private val portalHelper: PortalHelper
27-
): ViewModel() {
34+
private val portalHelper: PortalHelper,
35+
@ApplicationContext private val context: Context
36+
) : ViewModel() {
2837

2938
@AssistedFactory
3039
interface Factory {
@@ -37,6 +46,34 @@ class SessionViewModel @AssistedInject constructor(
3746
private val _session: MutableStateFlow<Session?> = MutableStateFlow(null)
3847
val session = _session.asStateFlow()
3948

49+
val startTime: Long
50+
get() = sdf.parse(session.value!!.start)!!.time
51+
52+
val endTime: Long
53+
get() = sdf.parse(session.value!!.end)!!.time
54+
55+
var bookmark: Boolean
56+
get() = sessionId in context.sharedPreferences.getBookmarks(eventId)
57+
set(value) = with(context.sharedPreferences) {
58+
val newSet = getBookmarks(eventId).toMutableSet()
59+
if (value) newSet.add(sessionId) else newSet.remove(sessionId)
60+
saveBookmarks(eventId, newSet)
61+
}
62+
63+
var alarm: Boolean
64+
get() = sessionId in context.sharedPreferences.getAlarms(eventId)
65+
set(value) = with(context.sharedPreferences) {
66+
val newSet = getAlarms(eventId).toMutableSet()
67+
if (value) {
68+
newSet.add(sessionId)
69+
AlarmUtil.setSessionAlarm(context, startTime, eventId, session.value!!)
70+
} else {
71+
newSet.remove(sessionId)
72+
AlarmUtil.cancelSessionAlarm(context, eventId, sessionId)
73+
}
74+
saveAlarms(eventId, newSet)
75+
}
76+
4077
init {
4178
getSession()
4279
}

0 commit comments

Comments
 (0)