Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {
id "kotlin-kapt"
id "com.google.gms.google-services"
id "com.google.firebase.crashlytics"
id "org.jetbrains.kotlin.plugin.compose"
}

def keystorePropertiesFile = rootProject.file("keystore.properties")
Expand Down Expand Up @@ -45,6 +46,7 @@ android {
buildFeatures {
viewBinding = true
dataBinding = true
compose = true
}


Expand All @@ -61,6 +63,20 @@ android {
}

dependencies {
def composeBom = platform('androidx.compose:compose-bom:2024.12.01')
implementation composeBom
androidTestImplementation composeBom

implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.material:material-icons-extended'
implementation 'androidx.activity:activity-compose:1.9.3'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7"
implementation "androidx.compose.runtime:runtime-livedata"
implementation "androidx.navigation:navigation-compose:2.8.5"

implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
Expand Down Expand Up @@ -89,6 +105,8 @@ android {
implementation "com.github.PhilJay:MPAndroidChart:v3.0.1"
implementation "com.github.bumptech.glide:glide:4.11.0"

testImplementation 'junit:junit:4.13.2'

ksp "androidx.room:room-compiler:$room_version"
runtimeOnly "androidx.room:room-runtime:$room_version"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import amrabed.android.release.evaluation.core.Record
import amrabed.android.release.evaluation.core.Task
import amrabed.android.release.evaluation.data.converters.ActiveDaysConverter
import amrabed.android.release.evaluation.data.converters.SelectionsConverter
import amrabed.android.release.evaluation.data.migrations.Migration2To3
import amrabed.android.release.evaluation.data.migrations.Migration3To4
import amrabed.android.release.evaluation.data.migrations.*
import amrabed.android.release.evaluation.data.tables.History
import amrabed.android.release.evaluation.data.tables.TaskTable
import amrabed.android.release.evaluation.data.tables.v2.*
import android.content.Context
import androidx.room.Database
import androidx.room.Room
Expand All @@ -17,11 +17,17 @@ import androidx.sqlite.db.SupportSQLiteDatabase
import java.util.concurrent.Executor
import java.util.concurrent.Executors

@Database(entities = [Record::class, Task::class], version = 4, exportSchema = false)
@Database(
entities = [Record::class, Task::class, RitualEntity::class, RitualLogEntity::class],
version = 5,
exportSchema = false
)
@TypeConverters(SelectionsConverter::class, ActiveDaysConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskTable(): TaskTable
abstract fun history(): History
abstract fun ritualDao(): RitualDao
abstract fun ritualLogDao(): RitualLogDao

class Callback : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
Expand All @@ -46,15 +52,17 @@ abstract class AppDatabase : RoomDatabase() {
if (database == null) {
synchronized(AppDatabase::class.java) {
if (database == null) {
database = Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java, DATABASE_NAME)
.addMigrations(Migration2To3(), Migration3To4())
.addCallback(Callback())
.build()
database = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java, DATABASE_NAME
)
.addMigrations(Migration2To3(), Migration3To4(), Migration4To5())
.addCallback(Callback())
.build()
}
}
}
return database
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package amrabed.android.release.evaluation.data.migrations

import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase

class Migration4To5 : Migration(4, 5) {
override fun migrate(db: SupportSQLiteDatabase) {
// Create Ritual table
db.execSQL("""
CREATE TABLE IF NOT EXISTS `rituals` (
`id` TEXT NOT NULL,
`title` TEXT,
`defaultIndex` INTEGER NOT NULL DEFAULT -1,
`prayerGroup` TEXT NOT NULL,
`sortOrder` INTEGER NOT NULL DEFAULT 0,
`activeDays` INTEGER NOT NULL DEFAULT 127,
`isHidden` INTEGER NOT NULL DEFAULT 0,
`createdAt` INTEGER NOT NULL,
`updatedAt` INTEGER NOT NULL,
PRIMARY KEY(`id`)
)
""".trimIndent())

// Create RitualLog table
db.execSQL("""
CREATE TABLE IF NOT EXISTS `ritual_logs` (
`id` TEXT NOT NULL,
`ritualId` TEXT NOT NULL,
`date` TEXT NOT NULL,
`completedAt` INTEGER,
`isComplete` INTEGER NOT NULL DEFAULT 0,
`note` TEXT,
PRIMARY KEY(`id`),
FOREIGN KEY(`ritualId`) REFERENCES `rituals`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE
)
""".trimIndent())

// Create indexes
db.execSQL("CREATE INDEX IF NOT EXISTS `index_ritual_logs_date` ON `ritual_logs` (`date`)")
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_ritual_logs_ritualId_date` ON `ritual_logs` (`ritualId`, `date`)")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package amrabed.android.release.evaluation.data.repository

import amrabed.android.release.evaluation.data.tables.v2.*
import amrabed.android.release.evaluation.domain.model.*
import amrabed.android.release.evaluation.domain.repository.PrayerTimeRepository
import amrabed.android.release.evaluation.domain.repository.RitualRepository
import kotlinx.coroutines.flow.*
import org.joda.time.LocalDate
import java.util.*

class RitualRepositoryImpl(
private val ritualDao: RitualDao,
private val ritualLogDao: RitualLogDao,
private val prayerTimeRepository: PrayerTimeRepository
) : RitualRepository {

override fun observeDailyChecklist(date: String): Flow<DailyChecklist> {
val jodaDate = LocalDate.parse(date)
val dayOfWeek = jodaDate.dayOfWeek

return combine(
ritualDao.getAllVisibleRituals(),
ritualLogDao.getLogsForDate(date)
) { rituals, logs ->
val ritualsWithLogs = rituals
.map { entity -> entity.toDomain() }
.filter { ritual -> ritual.isActiveOn(dayOfWeek) }
.map { ritual ->
RitualWithLog(ritual, logs.find { it.ritualId == ritual.id }?.toDomain())
}

val groups = ritualsWithLogs
.groupBy { it.ritual.prayerGroup }
.map { (group, rituals) ->
ChecklistGroup(
prayerGroup = group,
prayerTime = null, // Will be populated below
rituals = rituals
)
}
.sortedBy { it.prayerGroup.ordinal }

DailyChecklist(date, groups)
}.flatMapLatest { checklist ->
if (checklist.groups.isEmpty()) return@flatMapLatest flowOf(checklist)

val groupFlows = checklist.groups.map { group ->
prayerTimeRepository.getPrayerTime(group.prayerGroup.name, date).map { time ->
group.copy(prayerTime = time)
}
}
combine(groupFlows) { updatedGroups ->
checklist.copy(groups = updatedGroups.toList())
}
}
}

override suspend fun toggleRitual(ritualId: String, date: String, isComplete: Boolean) {
if (isComplete) {
val log = RitualLogEntity(
id = UUID.randomUUID().toString(),
ritualId = ritualId,
date = date,
completedAt = System.currentTimeMillis(),
isComplete = true
)
ritualLogDao.insertLog(log)
} else {
ritualLogDao.deleteLog(ritualId, date)
}
}

override suspend fun addRitual(ritual: Ritual) {
ritualDao.insertRitual(ritual.toEntity())
}

override suspend fun updateRitual(ritual: Ritual) {
ritualDao.updateRitual(ritual.toEntity())
}

override suspend fun hideRitual(ritualId: String) {
ritualDao.getAllRituals().first().find { it.id == ritualId }?.let {
ritualDao.updateRitual(it.copy(isHidden = true))
}
}

override suspend fun deleteRitual(ritualId: String) {
ritualDao.getAllRituals().first().find { it.id == ritualId }?.let {
if (it.defaultIndex < 0) {
ritualDao.deleteRitual(it)
} else {
hideRitual(ritualId)
}
}
}

override suspend fun reorderRituals(ritualIds: List<String>, prayerGroup: PrayerGroup) {
ritualIds.forEachIndexed { index, id ->
ritualDao.getAllRituals().first().find { it.id == id }?.let {
ritualDao.updateRitual(it.copy(sortOrder = index, prayerGroup = prayerGroup.name))
}
}
}

private fun RitualEntity.toDomain() = Ritual(
id = id,
title = title,
defaultIndex = defaultIndex,
prayerGroup = PrayerGroup.valueOf(prayerGroup),
sortOrder = sortOrder,
activeDays = activeDays,
isHidden = isHidden,
createdAt = createdAt,
updatedAt = updatedAt
)

private fun Ritual.toEntity() = RitualEntity(
id = id,
title = title,
defaultIndex = defaultIndex,
prayerGroup = prayerGroup.name,
sortOrder = sortOrder,
activeDays = activeDays,
isHidden = isHidden,
createdAt = createdAt,
updatedAt = updatedAt
)

private fun RitualLogEntity.toDomain() = RitualLog(
id = id,
ritualId = ritualId,
date = date,
completedAt = completedAt,
isComplete = isComplete,
note = note
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package amrabed.android.release.evaluation.data.repository

import amrabed.android.release.evaluation.data.tables.v2.RitualDao
import amrabed.android.release.evaluation.data.tables.v2.RitualEntity
import amrabed.android.release.evaluation.domain.model.PrayerGroup
import java.util.*

object RitualSeeder {
suspend fun seedIfEmpty(ritualDao: RitualDao) {
if (ritualDao.getRitualCount() == 0) {
val now = System.currentTimeMillis()
val rituals = mutableListOf<RitualEntity>()

// Pre-Fajr
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 0, PrayerGroup.PRE_FAJR.name, 0, 127, false, now, now)) // Wake up
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 1, PrayerGroup.PRE_FAJR.name, 1, 127, false, now, now)) // Siwak
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 32, PrayerGroup.PRE_FAJR.name, 2, 127, false, now, now)) // Wudu

// Fajr
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 5, PrayerGroup.FAJR.name, 3, 127, false, now, now)) // Fajr prayer
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 4, PrayerGroup.FAJR.name, 4, 127, false, now, now)) // Fajr sunnah
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 6, PrayerGroup.FAJR.name, 5, 127, false, now, now)) // Fajr azkar

// Morning
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 9, PrayerGroup.MORNING.name, 6, 127, false, now, now)) // Morning azkar
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 7, PrayerGroup.MORNING.name, 7, 127, false, now, now)) // Quran recitation
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 8, PrayerGroup.MORNING.name, 8, 127, false, now, now)) // Quran memorization
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 10, PrayerGroup.MORNING.name, 9, 127, false, now, now)) // Duha prayer
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 11, PrayerGroup.MORNING.name, 10, 127, false, now, now)) // Exercise
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 3, PrayerGroup.MORNING.name, 11, 9, false, now, now)) // Fasting (Mon=1, Thu=8 -> 1|8 = 9)
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 12, PrayerGroup.MORNING.name, 12, 16, false, now, now)) // Jumu'ah (Fri=16)

// Dhuhr/Asr
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 14, PrayerGroup.DHUHR_ASR.name, 13, 127, false, now, now)) // Congregational (Dhuhr)
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 15, PrayerGroup.DHUHR_ASR.name, 14, 127, false, now, now)) // Prayer azkar
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 16, PrayerGroup.DHUHR_ASR.name, 15, 127, false, now, now)) // Rawatib sunnah
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 13, PrayerGroup.DHUHR_ASR.name, 16, 127, false, now, now)) // Work / Study
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 17, PrayerGroup.DHUHR_ASR.name, 17, 127, false, now, now)) // Congregational Asr
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 19, PrayerGroup.DHUHR_ASR.name, 18, 127, false, now, now)) // Evening azkar

// Maghrib
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 20, PrayerGroup.MAGHRIB.name, 19, 127, false, now, now)) // Congregational Maghrib
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 21, PrayerGroup.MAGHRIB.name, 20, 127, false, now, now)) // Maghrib azkar
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 22, PrayerGroup.MAGHRIB.name, 21, 127, false, now, now)) // Rawatib sunnah

// Isha/Night
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 23, PrayerGroup.ISHA_NIGHT.name, 22, 127, false, now, now)) // Isha prayer
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 24, PrayerGroup.ISHA_NIGHT.name, 23, 127, false, now, now)) // Prayer azkar
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 25, PrayerGroup.ISHA_NIGHT.name, 24, 127, false, now, now)) // Rawatib sunnah
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 26, PrayerGroup.ISHA_NIGHT.name, 25, 127, false, now, now)) // Witr prayer
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 27, PrayerGroup.ISHA_NIGHT.name, 26, 127, false, now, now)) // Diet
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 28, PrayerGroup.ISHA_NIGHT.name, 27, 127, false, now, now)) // Good manners
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 29, PrayerGroup.ISHA_NIGHT.name, 28, 127, false, now, now)) // Honesty
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 30, PrayerGroup.ISHA_NIGHT.name, 29, 127, false, now, now)) // Avoiding backbiting
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 31, PrayerGroup.ISHA_NIGHT.name, 30, 127, false, now, now)) // Lowering the gaze
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 33, PrayerGroup.ISHA_NIGHT.name, 31, 127, false, now, now)) // Night azkar
rituals.add(RitualEntity(UUID.randomUUID().toString(), null, 32, PrayerGroup.ISHA_NIGHT.name, 32, 127, false, now, now)) // Sleep with wudu

ritualDao.insertRituals(rituals)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package amrabed.android.release.evaluation.data.tables.v2

import androidx.room.*
import kotlinx.coroutines.flow.Flow

@Dao
interface RitualDao {
@Query("SELECT * FROM rituals WHERE isHidden = 0 ORDER BY sortOrder ASC")
fun getAllVisibleRituals(): Flow<List<RitualEntity>>

@Query("SELECT * FROM rituals ORDER BY prayerGroup, sortOrder ASC")
fun getAllRituals(): Flow<List<RitualEntity>>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRitual(ritual: RitualEntity)

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRituals(rituals: List<RitualEntity>)

@Update
suspend fun updateRitual(ritual: RitualEntity)

@Delete
suspend fun deleteRitual(ritual: RitualEntity)

@Query("DELETE FROM rituals WHERE defaultIndex < 0")
suspend fun deleteAllCustomRituals()

@Query("SELECT COUNT(*) FROM rituals")
suspend fun getRitualCount(): Int
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package amrabed.android.release.evaluation.data.tables.v2

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "rituals")
data class RitualEntity(
@PrimaryKey val id: String,
val title: String?,
val defaultIndex: Int = -1,
val prayerGroup: String,
val sortOrder: Int,
val activeDays: Int = 127,
val isHidden: Boolean = false,
val createdAt: Long,
val updatedAt: Long
)
Loading