Skip to content

Commit

Permalink
feat: add reminders with hard coded editor data
Browse files Browse the repository at this point in the history
  • Loading branch information
JanMalch committed Dec 10, 2023
1 parent 1b97523 commit fca4cf9
Show file tree
Hide file tree
Showing 29 changed files with 1,906 additions and 10 deletions.
531 changes: 531 additions & 0 deletions app/schemas/io.github.janmalch.woroboro.data.AppDatabase/2.json

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand Down Expand Up @@ -39,5 +42,14 @@
android:name="photopicker_activity:0:required"
android:value="" />
</service>

<receiver android:name=".business.reminders.ReminderReceiver" />
<receiver
android:name=".business.reminders.BootCompletedReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
10 changes: 9 additions & 1 deletion app/src/main/kotlin/io/github/janmalch/woroboro/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,31 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import dagger.hilt.android.AndroidEntryPoint
import io.github.janmalch.woroboro.business.LaunchDataService
import io.github.janmalch.woroboro.business.reminders.AndroidReminderNotifications.Companion.getRoutineFilter
import io.github.janmalch.woroboro.ui.AppContainer
import io.github.janmalch.woroboro.ui.routine.ROUTINE_GRAPH_ROUTE
import io.github.janmalch.woroboro.ui.theme.WoroboroTheme
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

private val viewModel: MainViewModel by viewModels()

@Inject
lateinit var launchDataService: LaunchDataService

override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)

launchDataService.setRoutineFilter(intent.extras?.getRoutineFilter())

var uiState: MainUiState by mutableStateOf(MainUiState.Loading)

// Update the uiState
Expand All @@ -50,11 +58,11 @@ class MainActivity : ComponentActivity() {
}
}


setContent {
WoroboroTheme {
AppContainer(startDestination = ROUTINE_GRAPH_ROUTE)
}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class MainViewModel @Inject constructor(

) : ViewModel() {

// FIXME: remove all this until needed
val state = flowOf(MainUiState.Success)
.stateIn(
scope = viewModelScope,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import io.github.janmalch.woroboro.business.reminders.AndroidReminderNotifications
import io.github.janmalch.woroboro.business.reminders.AndroidReminderScheduler
import io.github.janmalch.woroboro.business.reminders.ReminderNotifications
import io.github.janmalch.woroboro.business.reminders.ReminderScheduler
import javax.inject.Singleton


Expand All @@ -23,6 +27,16 @@ interface BusinessModule {
@Singleton
fun bindsRoutineRepository(impl: RoutineRepositoryImpl): RoutineRepository

@Binds
@Singleton
fun bindsReminderRepository(impl: ReminderRepositoryImpl): ReminderRepository

@Binds
fun bindsReminderScheduler(impl: AndroidReminderScheduler): ReminderScheduler

@Binds
fun bindsReminderNotifications(impl: AndroidReminderNotifications): ReminderNotifications

@Binds
@Singleton
fun bindsMediaFileManager(impl: MediaFileManagerImpl): MediaFileManager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package io.github.janmalch.woroboro.business
import io.github.janmalch.woroboro.data.model.ExerciseEntity
import io.github.janmalch.woroboro.data.model.ExerciseEntityWithMediaAndTags
import io.github.janmalch.woroboro.data.model.MediaEntity
import io.github.janmalch.woroboro.data.model.ReminderEntity
import io.github.janmalch.woroboro.data.model.RoutineEntity
import io.github.janmalch.woroboro.data.model.RoutineStepEntity
import io.github.janmalch.woroboro.data.model.TagEntity
import io.github.janmalch.woroboro.models.Exercise
import io.github.janmalch.woroboro.models.FullRoutine
import io.github.janmalch.woroboro.models.Media
import io.github.janmalch.woroboro.models.Reminder
import io.github.janmalch.woroboro.models.Routine
import io.github.janmalch.woroboro.models.RoutineStep
import io.github.janmalch.woroboro.models.Tag
Expand Down Expand Up @@ -78,3 +80,15 @@ fun FullRoutine.asEntities(): Pair<RoutineEntity, List<RoutineStepEntity>> = Rou
lastRunDuration = lastRunDuration,
lastRunEnded = lastRunEnded,
) to steps.map { it.asEntity(id) }


fun Reminder.asEntities(): Pair<ReminderEntity, List<String>> = ReminderEntity(
id = id,
name = name,
remindAt = remindAt,
weekdays = weekdays,
repeatEvery = repeat?.every,
repeatUntil = repeat?.until,
filterOnlyFavorites = filter.onlyFavorites,
filterDuration = filter.durationFilter,
) to filter.selectedTags.map { it.label }
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.github.janmalch.woroboro.business

import android.util.Log
import io.github.janmalch.woroboro.models.RoutineFilter
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class LaunchDataService @Inject constructor() {

private var routineFilter: RoutineFilter? = null

fun setRoutineFilter(routineFilter: RoutineFilter?) {
if (routineFilter != null) {
Log.w("LaunchDataRepository", "A routine filter is already set. Overwriting it.")
}
this.routineFilter = routineFilter
}

fun consumeRoutineFilter(): RoutineFilter? {
val filter = routineFilter
routineFilter = null
return filter
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.github.janmalch.woroboro.business

import androidx.room.withTransaction
import io.github.janmalch.woroboro.business.reminders.ReminderScheduler
import io.github.janmalch.woroboro.data.AppDatabase
import io.github.janmalch.woroboro.data.dao.ReminderDao
import io.github.janmalch.woroboro.data.model.ReminderEntityWithFilterTags
import io.github.janmalch.woroboro.data.model.asModel
import io.github.janmalch.woroboro.models.Reminder
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.util.UUID
import javax.inject.Inject

interface ReminderRepository {
fun findAll(): Flow<List<Reminder>>

fun findOne(id: UUID): Flow<Reminder?>

suspend fun insert(reminder: Reminder): UUID

suspend fun update(reminder: Reminder): UUID

suspend fun delete(reminderId: UUID)
}

class ReminderRepositoryImpl @Inject constructor(
private val database: AppDatabase,
private val reminderDao: ReminderDao,
private val reminderScheduler: ReminderScheduler,
) : ReminderRepository {
override fun findAll(): Flow<List<Reminder>> {
return reminderDao.findAll().map { list -> list.map(ReminderEntityWithFilterTags::asModel) }
}

override fun findOne(id: UUID): Flow<Reminder?> {
return reminderDao.findOne(id).map { it?.asModel() }
}

override suspend fun insert(reminder: Reminder): UUID {
val model = reminder.copy(id = UUID.randomUUID())
val (entity, filterTags) = model.asEntities()
reminderDao.upsert(entity, filterTags)
reminderScheduler.schedule(model)
return model.id
}

override suspend fun update(reminder: Reminder): UUID {
return database.withTransaction {
// delete and insert for new ID,
// so that notifications don't get weird (?)
delete(reminder.id)
insert(reminder)
}
}

override suspend fun delete(reminderId: UUID) {
reminderScheduler.cancel(reminderId)
reminderDao.delete(reminderId)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import io.github.janmalch.woroboro.data.dao.RoutineDao
import io.github.janmalch.woroboro.data.model.RoutineStepEntity
import io.github.janmalch.woroboro.models.DurationFilter
import io.github.janmalch.woroboro.models.FullRoutine
import io.github.janmalch.woroboro.models.Reminder
import io.github.janmalch.woroboro.models.Routine
import io.github.janmalch.woroboro.models.RoutineStep
import io.github.janmalch.woroboro.models.Tag
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
Expand Down Expand Up @@ -43,6 +45,15 @@ interface RoutineRepository {

}

fun RoutineRepository.findByReminder(reminder: Reminder): Flow<List<Routine>> {
return findAll(
tags = reminder.filter.selectedTags.map(Tag::label),
onlyFavorites = reminder.filter.onlyFavorites,
durationFilter = reminder.filter.durationFilter,
textQuery = "",
)
}

class RoutineRepositoryImpl @Inject constructor(
private val routineDao: RoutineDao
) : RoutineRepository {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.github.janmalch.woroboro.business.reminders

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import dagger.hilt.android.AndroidEntryPoint
import io.github.janmalch.woroboro.business.ReminderRepository
import io.github.janmalch.woroboro.utils.ApplicationScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject

@AndroidEntryPoint
class BootCompletedReceiver : BroadcastReceiver() {

@Inject
lateinit var scheduler: ReminderScheduler

@Inject
lateinit var repository: ReminderRepository

@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope

override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
Log.d(
"BootCompletedReceiver",
"Received boot completed event. Rescheduling all reminders."
)
applicationScope.launch {
val reminders = repository.findAll().first()
reminders.forEach(scheduler::schedule)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package io.github.janmalch.woroboro.business.reminders

import android.Manifest
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.janmalch.woroboro.MainActivity
import io.github.janmalch.woroboro.models.Reminder
import io.github.janmalch.woroboro.models.RoutineFilter
import javax.inject.Inject


interface ReminderNotifications {
fun show(reminder: Reminder, image: Bitmap? = null)
}

class AndroidReminderNotifications @Inject constructor(
@ApplicationContext private val context: Context,
) : ReminderNotifications {
override fun show(reminder: Reminder, image: Bitmap?) {
createNotificationChannel()

val intent = Intent(context, MainActivity::class.java).apply {
// TODO: revisit flags
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(INTENT_EXTRA_FILTER, reminder.filter)
}
val pendingIntent =
PendingIntent.getActivity(
context,
reminder.id.hashCode(),
intent,
PendingIntent.FLAG_IMMUTABLE
)

val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_lock_idle_alarm)
.setContentTitle(reminder.name)
.setContentText("Es ist Zeit für eine deiner Routinen!") // TODO: i18n
.setLargeIcon(image)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(Notification.CATEGORY_REMINDER)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(pendingIntent)
.setAutoCancel(true) // remove on tap
.setOnlyAlertOnce(false) // alert again if same reminder triggers again
.build()

with(NotificationManagerCompat.from(context)) {
if (
ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
notify(reminder.id.hashCode(), notification)
}
}
}

private fun createNotificationChannel() {
val name = "Woroboro Reminders" // TODO: i18n
val descriptionText =
"Benachrichtigungen für deine selbsterstellten Erinnerungen." // TODO: i18n
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
description = descriptionText
}
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}

companion object {
const val CHANNEL_ID = "Woroboro Reminders"
private const val INTENT_EXTRA_FILTER = "filters"

fun Bundle.getRoutineFilter(): RoutineFilter? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelable(INTENT_EXTRA_FILTER, RoutineFilter::class.java)
} else {
getParcelable(INTENT_EXTRA_FILTER)
}
}
}
Loading

0 comments on commit fca4cf9

Please sign in to comment.