Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@ import com.instructure.horizon.database.dao.HorizonLocalFileDao
import com.instructure.horizon.database.dao.HorizonPageDao
import com.instructure.horizon.database.dao.HorizonProgramDao
import com.instructure.horizon.database.dao.HorizonEntitySyncMetadataDao
import com.instructure.horizon.database.dao.HorizonCourseSyncPlanDao
import com.instructure.horizon.database.dao.HorizonFileSyncPlanDao
import com.instructure.horizon.database.dao.HorizonLocalImageDao
import com.instructure.horizon.database.dao.HorizonSubmissionDao
import com.instructure.horizon.database.dao.HorizonSyncMetadataDao
import com.instructure.horizon.database.dao.HorizonSyncSettingsDao
import com.instructure.horizon.database.dao.HorizonUserDao
import com.instructure.horizon.di.HorizonHtmlParserQualifier
import com.instructure.horizon.di.HorizonOfflineModule
import com.instructure.horizon.offline.HorizonHtmlParserFileSource
Expand Down Expand Up @@ -110,6 +115,21 @@ object HorizonOfflineTestModule {
@Provides
fun provideHorizonSubmissionDao(db: HorizonDatabase): HorizonSubmissionDao = db.submissionDao()

@Provides
fun provideHorizonSyncSettingsDao(db: HorizonDatabase): HorizonSyncSettingsDao = db.syncSettingsDao()

@Provides
fun provideHorizonCourseSyncPlanDao(db: HorizonDatabase): HorizonCourseSyncPlanDao = db.courseSyncPlanDao()

@Provides
fun provideHorizonFileSyncPlanDao(db: HorizonDatabase): HorizonFileSyncPlanDao = db.fileSyncPlanDao()

@Provides
fun provideHorizonLocalImageDao(db: HorizonDatabase): HorizonLocalImageDao = db.localImageDao()

@Provides
fun provideHorizonUserDao(db: HorizonDatabase): HorizonUserDao = db.userDao()

@Provides
@HorizonHtmlParserQualifier
fun provideHorizonHtmlParser(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2026 - present Instructure, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.instructure.horizon.data.datasource

import com.instructure.canvasapi2.models.User
import com.instructure.horizon.database.dao.HorizonUserDao
import com.instructure.horizon.database.entity.HorizonUserEntity
import javax.inject.Inject

class AccountLocalDataSource @Inject constructor(
private val userDao: HorizonUserDao,
) {
suspend fun saveUser(user: User) {
userDao.upsert(
HorizonUserEntity(
id = user.id,
name = user.name,
shortName = user.shortName,
)
)
}

suspend fun getUser(): User? {
val entity = userDao.getUser() ?: return null
return User(
id = entity.id,
name = entity.name,
shortName = entity.shortName,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress
import com.instructure.canvasapi2.managers.graphql.horizon.journey.Program
import com.instructure.canvasapi2.managers.graphql.horizon.journey.ProgramRequirement
import com.instructure.horizon.database.dao.HorizonCourseDao
import com.instructure.horizon.database.dao.HorizonEntitySyncMetadataDao
import com.instructure.horizon.database.dao.HorizonProgramDao
import com.instructure.horizon.database.dao.HorizonSyncMetadataDao
import com.instructure.horizon.database.entity.EntitySyncType
import com.instructure.horizon.database.entity.HorizonCourseEntity
import com.instructure.horizon.database.entity.HorizonEntitySyncMetadataEntity
import com.instructure.horizon.database.entity.HorizonProgramCourseRef
import com.instructure.horizon.database.entity.HorizonProgramEntity
import com.instructure.horizon.database.entity.HorizonSyncMetadataEntity
Expand All @@ -35,6 +38,7 @@ class CourseDetailsLocalDataSource @Inject constructor(
private val courseDao: HorizonCourseDao,
private val programDao: HorizonProgramDao,
private val syncMetadataDao: HorizonSyncMetadataDao,
private val entitySyncMetadataDao: HorizonEntitySyncMetadataDao,
) {

suspend fun getCourse(courseId: Long): CourseWithProgress {
Expand Down Expand Up @@ -78,12 +82,16 @@ class CourseDetailsLocalDataSource @Inject constructor(
}
programDao.insertAll(programEntities)
programDao.insertAllRefs(refs)
val now = System.currentTimeMillis()
syncMetadataDao.upsert(
HorizonSyncMetadataEntity(
dataType = SyncDataType.COURSE_DETAILS,
lastSyncedAtMs = System.currentTimeMillis(),
lastSyncedAtMs = now,
)
)
entitySyncMetadataDao.upsert(
HorizonEntitySyncMetadataEntity(EntitySyncType.COURSE, course.courseId, now)
)
}

private fun HorizonCourseEntity.toCourseWithProgress(): CourseWithProgress {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.instructure.horizon.data.datasource

import com.instructure.canvasapi2.utils.toDate
import com.instructure.horizon.database.dao.HorizonCourseModuleDao
import com.instructure.horizon.database.dao.HorizonDashboardModuleItemDao
import com.instructure.horizon.database.entity.HorizonDashboardModuleItemEntity
import com.instructure.horizon.model.DashboardNextModuleItem
Expand All @@ -24,18 +26,33 @@ import javax.inject.Inject

class ModuleItemLocalDataSource @Inject constructor(
private val moduleItemDao: HorizonDashboardModuleItemDao,
private val courseModuleDao: HorizonCourseModuleDao,
) {

suspend fun getNextModuleItemForCourse(courseId: Long): DashboardNextModuleItem? {
val entity = moduleItemDao.getFirstForCourse(courseId) ?: return null
val courseModuleItem = courseModuleDao.getNextModuleItemForCourse(courseId)
if (courseModuleItem != null) {
return DashboardNextModuleItem(
moduleItemId = courseModuleItem.itemId,
courseId = courseModuleItem.courseId,
title = courseModuleItem.title.orEmpty(),
type = if (courseModuleItem.quizLti) LearningObjectType.ASSESSMENT
else LearningObjectType.fromApiString(courseModuleItem.type.orEmpty()),
dueDate = courseModuleItem.dueAt?.toDate(),
estimatedDuration = courseModuleItem.estimatedDuration,
isQuizLti = courseModuleItem.quizLti,
)
}

val dashboardItem = moduleItemDao.getFirstForCourse(courseId) ?: return null
return DashboardNextModuleItem(
moduleItemId = entity.moduleItemId,
courseId = entity.courseId,
title = entity.moduleItemTitle,
type = LearningObjectType.valueOf(entity.moduleItemType),
dueDate = entity.dueDateMs?.let { Date(it) },
estimatedDuration = entity.estimatedDuration,
isQuizLti = entity.isQuizLti,
moduleItemId = dashboardItem.moduleItemId,
courseId = dashboardItem.courseId,
title = dashboardItem.moduleItemTitle,
type = LearningObjectType.valueOf(dashboardItem.moduleItemType),
dueDate = dashboardItem.dueDateMs?.let { Date(it) },
estimatedDuration = dashboardItem.estimatedDuration,
isQuizLti = dashboardItem.isQuizLti,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ package com.instructure.horizon.data.datasource
import com.instructure.canvasapi2.managers.graphql.horizon.journey.Program
import com.instructure.canvasapi2.managers.graphql.horizon.journey.ProgramRequirement
import com.instructure.horizon.database.dao.HorizonProgramDao
import com.instructure.horizon.database.dao.HorizonSyncMetadataDao
import com.instructure.horizon.database.entity.HorizonProgramCourseRef
import com.instructure.horizon.database.entity.HorizonProgramEntity
import com.instructure.horizon.database.entity.HorizonSyncMetadataEntity
import com.instructure.horizon.database.entity.SyncDataType
import com.instructure.journey.type.ProgramProgressCourseEnrollmentStatus
import com.instructure.journey.type.ProgramVariantType
import java.util.Date
import javax.inject.Inject

class ProgramLocalDataSource @Inject constructor(
private val programDao: HorizonProgramDao,
private val syncMetadataDao: HorizonSyncMetadataDao,
) {

suspend fun getPrograms(): List<Program> {
Expand Down Expand Up @@ -56,7 +60,7 @@ class ProgramLocalDataSource @Inject constructor(
}
}

suspend fun savePrograms(programs: List<Program>, enrolledCourseIds: Set<Long>) {
suspend fun savePrograms(programs: List<Program>) {
val programEntities = programs.map { program ->
HorizonProgramEntity(
programId = program.id,
Expand All @@ -75,7 +79,6 @@ class ProgramLocalDataSource @Inject constructor(
}
val refs = programs.flatMap { program ->
program.sortedRequirements
.filter { it.courseId in enrolledCourseIds }
.mapIndexed { index, req ->
HorizonProgramCourseRef(
programId = program.id,
Expand All @@ -90,5 +93,11 @@ class ProgramLocalDataSource @Inject constructor(
}
}
programDao.replaceAll(programEntities, refs)
syncMetadataDao.upsert(
HorizonSyncMetadataEntity(
dataType = SyncDataType.DASHBOARD_PROGRAMS,
lastSyncedAtMs = System.currentTimeMillis(),
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import com.instructure.canvasapi2.models.Assignment
import com.instructure.horizon.data.datasource.AssignmentDetailsLocalDataSource
import com.instructure.horizon.data.datasource.AssignmentDetailsNetworkDataSource
import com.instructure.horizon.data.datasource.SubmissionLocalDataSource
import com.instructure.horizon.database.dao.HorizonCourseModuleDao
import com.instructure.horizon.database.dao.HorizonEntitySyncMetadataDao
import com.instructure.horizon.database.entity.EntitySyncType
import com.instructure.horizon.database.entity.HorizonEntitySyncMetadataEntity
import com.instructure.horizon.di.HorizonHtmlParserQualifier
import com.instructure.horizon.offline.OfflineSyncRepository
import com.instructure.pandautils.features.offline.sync.HtmlParser
Expand All @@ -30,6 +34,8 @@ class AssignmentDetailsRepository @Inject constructor(
private val networkDataSource: AssignmentDetailsNetworkDataSource,
private val localDataSource: AssignmentDetailsLocalDataSource,
private val submissionLocalDataSource: SubmissionLocalDataSource,
private val courseModuleDao: HorizonCourseModuleDao,
private val entitySyncMetadataDao: HorizonEntitySyncMetadataDao,
@HorizonHtmlParserQualifier private val htmlParser: HtmlParser,
private val fileSyncRepository: HorizonFileSyncRepository,
networkStateProvider: NetworkStateProvider,
Expand All @@ -45,6 +51,7 @@ class AssignmentDetailsRepository @Inject constructor(
fileSyncRepository.syncHtmlFiles(courseId, parsingResult)
val submissionHistory = assignment.submission?.submissionHistory?.filterNotNull() ?: emptyList()
submissionLocalDataSource.saveSubmissions(assignment.id, submissionHistory)
updateModuleItemSyncTimestamp(assignmentId)
}
}
} else {
Expand All @@ -53,7 +60,10 @@ class AssignmentDetailsRepository @Inject constructor(
}
}

override suspend fun sync() {
TODO("Not yet implemented")
private suspend fun updateModuleItemSyncTimestamp(contentId: Long) {
val moduleItemId = courseModuleDao.getItemIdByContentId(contentId) ?: return
entitySyncMetadataDao.upsert(
HorizonEntitySyncMetadataEntity(EntitySyncType.MODULE_ITEM, moduleItemId, System.currentTimeMillis())
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,4 @@ class CourseEnrollmentRepository @Inject constructor(
suspend fun acceptInvite(courseId: Long, enrollmentId: Long) {
networkDataSource.acceptInvite(courseId, enrollmentId)
}

override suspend fun sync() {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,4 @@ class CourseFilesRepository @Inject constructor(
return localDataSource.getSyncedFileIds(courseId)
}

override suspend fun sync() = Unit
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,4 @@ class CourseRepository @Inject constructor(
}
}

override suspend fun sync() {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,4 @@ class CourseScoreRepository @Inject constructor(
localDataSource.saveScoreData(courseId, assignmentGroups, enrollments)
}
}

override suspend fun sync() {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,4 @@ class FileContentRepository @Inject constructor(
private fun extractFileId(url: String): Long? {
return Regex("files/(\\d+)").find(url)?.groupValues?.get(1)?.toLongOrNull()
}

override suspend fun sync() {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,25 @@ class HorizonFileSyncRepository @Inject constructor(
downloadInternalFile(fileId, courseId)
}

suspend fun downloadFileByUrl(fileId: Long, courseId: Long, url: String, displayName: String) {
if (localFileDao.findById(fileId) != null) return
val dir = File(context.filesDir, apiPrefs.user?.id.toString()).also { it.mkdirs() }
val destFile = File(dir, "${fileId}_$displayName")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If apiPrefs.user is null, apiPrefs.user?.id.toString() evaluates to the string "null", and files will be placed in <filesDir>/null/. This directory won't be cleaned up correctly by CourseCleanupHelper or DeleteSyncedContentUseCase (which looks for <filesDir>/<userId>). Guard against this:

val userId = apiPrefs.user?.id ?: return
val dir = File(context.filesDir, userId.toString()).also { it.mkdirs() }


if (destFile.exists()) return

downloadToFile(url, destFile, shouldIgnoreToken = false) {
localFileDao.insert(
HorizonLocalFileEntity(
fileId,
courseId,
Date(),
destFile.absolutePath
)
)
}
}

suspend fun syncHtmlFiles(courseId: Long, parsingResult: HtmlParsingResult) = withContext(Dispatchers.IO) {
val alreadyDownloadedIds = localFileDao.findByCourseId(courseId).map { it.id }.toSet()
val internalFileIdsToSync = parsingResult.internalFileIds.filterNot { alreadyDownloadedIds.contains(it) }
Expand Down Expand Up @@ -118,7 +137,4 @@ class HorizonFileSyncRepository @Inject constructor(
}
}

override suspend fun sync() {
TODO("Not yet implemented — will sync all/selected course files")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,4 @@ class LearnLearningLibraryRepository @Inject constructor(
}
}

override suspend fun sync() {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,4 @@ class LearnMyContentRepository @Inject constructor(
localDataSource.saveLearnItems(allItems, queryKey)
}

override suspend fun sync() {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,4 @@ class ModuleItemRepository @Inject constructor(
localDataSource.getNextModuleItemForCourse(courseId)
}
}

override suspend fun sync() {
TODO("Not yet implemented")
}
}
Loading
Loading