Skip to content

Commit

Permalink
feat: add reminders only for a specific routine
Browse files Browse the repository at this point in the history
  • Loading branch information
JanMalch committed Dec 20, 2023
1 parent b067fdf commit 18df089
Show file tree
Hide file tree
Showing 15 changed files with 294 additions and 62 deletions.
8 changes: 7 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@
android:theme="@style/Theme.Woroboro">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:scheme="woroboro"
android:host="routines" />
</intent-filter>
</activity>

<activity
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package io.github.janmalch.woroboro.business

import android.util.Log
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.RoutineQuery
import io.github.janmalch.woroboro.models.RoutineStep
import io.github.janmalch.woroboro.models.Tag
import io.github.janmalch.woroboro.models.asRoutineFilter
import io.github.janmalch.woroboro.models.asRoutine
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
Expand All @@ -27,10 +25,10 @@ interface RoutineRepository {
* Otherwise returns both favorites and non-favorites.
*/
fun findAll(
tags: List<String>,
onlyFavorites: Boolean,
durationFilter: DurationFilter,
textQuery: String,
tags: List<String> = emptyList(),
onlyFavorites: Boolean = false,
durationFilter: DurationFilter = DurationFilter.Any,
textQuery: String = "",
): Flow<List<Routine>>

fun findOne(id: UUID): Flow<FullRoutine?>
Expand All @@ -48,20 +46,17 @@ interface RoutineRepository {

}

fun RoutineRepository.findByReminder(reminder: Reminder): Flow<List<Routine>> {
if (reminder.query !is RoutineQuery.RoutineFilter) {
Log.w(
"RoutineRepository",
"findByReminder called with a query that is not a RoutineFilter."
fun RoutineRepository.findByQuery(query: RoutineQuery): Flow<List<Routine>> {
return when (query) {
is RoutineQuery.RoutineFilter -> findAll(
tags = query.selectedTags.map(Tag::label),
onlyFavorites = query.onlyFavorites,
durationFilter = query.durationFilter,
)

is RoutineQuery.Single -> findOne(query.routineId)
.map { it?.asRoutine()?.let(::listOf) ?: emptyList() }
}
val filter = reminder.query.asRoutineFilter()
return findAll(
tags = filter.selectedTags.map(Tag::label),
onlyFavorites = filter.onlyFavorites,
durationFilter = filter.durationFilter,
textQuery = "",
)
}

class RoutineRepositoryImpl @Inject constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,72 @@ import android.os.Bundle
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.janmalch.woroboro.MainActivity
import io.github.janmalch.woroboro.R
import io.github.janmalch.woroboro.models.Reminder
import io.github.janmalch.woroboro.models.RoutineQuery
import io.github.janmalch.woroboro.ui.routine.routine.ROUTINE_SCREEN_DEEPLINK
import javax.inject.Inject


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

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

// TODO: if ReminderQuery.Single -> DeepLink?
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.query)
val contentText = content ?: context.getString(R.string.reminder_notification_content)

val pendingIntent: PendingIntent? = when (reminder.query) {
is RoutineQuery.RoutineFilter -> {
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.query)
}

PendingIntent.getActivity(
context,
reminder.id.hashCode(),
intent,
PendingIntent.FLAG_IMMUTABLE
)
}

is RoutineQuery.Single -> {
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
(ROUTINE_SCREEN_DEEPLINK + reminder.query.routineId).toUri(),
context,
MainActivity::class.java
)

TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE)
}
}
}
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(context.getString(R.string.reminder_notification_content))
.setContentText(contentText)
.setLargeIcon(image)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(Notification.CATEGORY_REMINDER)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@ package io.github.janmalch.woroboro.business.reminders
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.core.net.toFile
import dagger.hilt.android.AndroidEntryPoint
import io.github.janmalch.woroboro.R
import io.github.janmalch.woroboro.business.ReminderRepository
import io.github.janmalch.woroboro.business.RoutineRepository
import io.github.janmalch.woroboro.business.findByReminder
import io.github.janmalch.woroboro.business.findByQuery
import io.github.janmalch.woroboro.models.Reminder
import io.github.janmalch.woroboro.models.Routine
import io.github.janmalch.woroboro.models.RoutineQuery
import io.github.janmalch.woroboro.utils.ApplicationScope
import io.github.janmalch.woroboro.utils.formatForTimer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -46,11 +50,11 @@ class ReminderReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val reminderId = intent?.getReminderId() ?: return
applicationScope.launch {
handle(reminderId)
handle(context, reminderId)
}
}

private suspend fun handle(reminderId: UUID) {
private suspend fun handle(context: Context?, reminderId: UUID) {
val reminder = repository.findOne(reminderId).firstOrNull()

if (reminder == null) {
Expand All @@ -64,11 +68,32 @@ class ReminderReceiver : BroadcastReceiver() {
}

if (reminder.isDueNow()) {
val image = try {
routines.findByReminder(reminder).firstOrNull()
var image: Bitmap? = null
var content: String? = null
try {
val routines = routines.findByQuery(reminder.query).firstOrNull()
?.takeUnless(List<Routine>::isEmpty)
?.asSequence()
?.flatMap { it.media }

if (context != null && reminder.query is RoutineQuery.Single && routines != null) {
val routine = routines.firstOrNull()
if (routine != null) {
content = if (routine.lastRunDuration != null) {
context.getString(
R.string.reminder_notification_content_single,
routine.name,
formatForTimer(routine.lastRunDuration),
)
} else {
context.getString(
R.string.reminder_notification_content_single_no_last_run,
routine.name,
)
}
}
}

image = routines?.flatMap { it.media }
?.shuffled()
?.firstOrNull()
?.thumbnail
Expand All @@ -77,12 +102,11 @@ class ReminderReceiver : BroadcastReceiver() {
} catch (e: Exception) {
Log.w(
"ReminderReceiver",
"Error while trying to create bitmap for reminder $reminderId.",
"Error while trying to create image or text for reminder $reminderId.",
e
)
null
}
notifications.show(reminder, image)
notifications.show(reminder, image, content)
} else {
Log.d(
"ReminderReceiver",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ fun FavoriteIcon(
fun OnlyFavoritesChip(
value: Boolean,
onValueChange: (Boolean) -> Unit,
enabled: Boolean = true,
) {
FilterChip(
selected = value,
label = { FavoriteIcon(isFavorite = value, modifier = Modifier.size(18.dp)) },
onClick = { onValueChange(!value) }
onClick = { onValueChange(!value) },
enabled = enabled,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import io.github.janmalch.woroboro.models.DurationFilter
fun DurationFilterChip(
value: DurationFilter,
onValueChange: (DurationFilter) -> Unit,
enabled: Boolean = true,
) {
var expanded by rememberSaveable { mutableStateOf(false) }
val emitAndClose = remember<(DurationFilter) -> Unit>(onValueChange) {
Expand All @@ -50,7 +51,8 @@ fun DurationFilterChip(
)
},
selected = value != DurationFilter.Any,
onClick = { expanded = true }
onClick = { expanded = true },
enabled = enabled,
)

DropdownMenu(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ fun RoutineFilterRow(
onOnlyFavoritesChange: (Boolean) -> Unit,
onSelectedTagsChange: (List<Tag>) -> Unit,
containerColor: Color = Color.Unspecified,
contentPadding: PaddingValues = PaddingValues(vertical = 0.dp, horizontal = 12.dp)
contentPadding: PaddingValues = PaddingValues(vertical = 0.dp, horizontal = 12.dp),
enabled: Boolean = true,
) {
Row(
modifier = androidx.compose.ui.Modifier
Expand All @@ -41,16 +42,19 @@ fun RoutineFilterRow(
OnlyFavoritesChip(
value = isOnlyFavorites,
onValueChange = onOnlyFavoritesChange,
enabled = enabled,
)
DurationFilterChip(
value = durationFilter,
onValueChange = onDurationFilterChange,
enabled = enabled,
)
TagSelectors(
availableTags = availableTags,
value = selectedTags,
isCounterVisible = false,
onValueChange = onSelectedTagsChange,
enabled = enabled,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ fun TagSelectors(
value: List<Tag>,
isCounterVisible: Boolean,
onValueChange: (List<Tag>) -> Unit,
enabled: Boolean = true,
) {
availableTags.forEach { (type, labels) ->
val valuesForType = remember(type, value) {
Expand All @@ -50,7 +51,8 @@ fun TagSelectors(
// and append new ones
(newValuesForType.map { Tag(label = it, type = type) })
)
}
},
enabled = enabled,
)
}
}
Expand All @@ -62,6 +64,7 @@ fun TagTypeMultiDropdown(
value: List<String>,
isCounterVisible: Boolean,
onValueChange: (List<String>) -> Unit,
enabled: Boolean = true,
) {
var expanded by rememberSaveable { mutableStateOf(false) }
val clearFocus = rememberClearFocus()
Expand All @@ -82,7 +85,8 @@ fun TagTypeMultiDropdown(
} else null,
trailingIcon = {
Icon(Icons.Default.ArrowDropDown, contentDescription = null)
}
},
enabled = enabled,
)

DropdownMenu(
Expand Down
Loading

0 comments on commit 18df089

Please sign in to comment.