diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 43d01f8..767a04b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:fullBackupContent="@xml/backup_rules" android:icon="@drawable/icon" android:label="@string/app_name" + android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.VyatsuSchedule"> diff --git a/app/src/main/java/agzam/vyatsu/data/local/TeacherScheduleDao.kt b/app/src/main/java/agzam/vyatsu/data/local/TeacherScheduleDao.kt index 4ae978f..57b0684 100644 --- a/app/src/main/java/agzam/vyatsu/data/local/TeacherScheduleDao.kt +++ b/app/src/main/java/agzam/vyatsu/data/local/TeacherScheduleDao.kt @@ -33,6 +33,9 @@ interface TeacherScheduleDao { @Query("DELETE FROM teacher_schedules") suspend fun deleteAll() + @Query("SELECT lastSyncedAt FROM teacher_schedules WHERE departmentId = :departmentId AND season = :season AND weekStart = :weekStart") + suspend fun lastSyncedAt(departmentId: String, season: String, weekStart: Long): Long? + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertDays(days: List): List @@ -41,6 +44,7 @@ interface TeacherScheduleDao { @Transaction suspend fun replaceSchedule(schedule: TeacherScheduleEntity, days: List, lessons: List) { + deleteSchedule(schedule.departmentId, schedule.season.name, schedule.weekStart) val scheduleId = insertSchedule(schedule) val daysWithScheduleId = days.map { it.copy(scheduleId = scheduleId) } deleteDaysForSchedule(scheduleId) diff --git a/app/src/main/java/agzam/vyatsu/data/repository/TeacherScheduleRepositoryImpl.kt b/app/src/main/java/agzam/vyatsu/data/repository/TeacherScheduleRepositoryImpl.kt index 8f9b44c..af52956 100644 --- a/app/src/main/java/agzam/vyatsu/data/repository/TeacherScheduleRepositoryImpl.kt +++ b/app/src/main/java/agzam/vyatsu/data/repository/TeacherScheduleRepositoryImpl.kt @@ -56,7 +56,7 @@ class TeacherScheduleRepositoryImpl( scheduleDao.observeDistinctTeachers(period.season.name, period.weekStart.toEpochDay()) .map { names -> names.map { name -> Teacher(name = name) } } - override suspend fun syncPeriod(date: LocalDate): Flow = flow { + override suspend fun syncPeriod(date: LocalDate, force: Boolean): Flow = flow { // Unique index (departmentId, season, weekStart) breaks on interleaved // delete+insert from concurrent syncPeriod() callers. syncMutex.withLock { @@ -68,18 +68,27 @@ class TeacherScheduleRepositoryImpl( return@withLock } - targets.map { it.season to it.weekStart } - .distinct() - .forEach { (season, weekStart) -> - withContext(Dispatchers.IO) { - scheduleDao.deletePeriod(season.name, weekStart) - } + val now = now() + var skippedCount = 0 + val nonSkipped = mutableListOf() + for (target in targets) { + val last = withContext(Dispatchers.IO) { + scheduleDao.lastSyncedAt(target.departmentId, target.season.name, target.weekStart) } + val shouldSkip = !force && last != null && now - last < STALE_WINDOW_MS + if (shouldSkip) { + skippedCount++ + } else { + nonSkipped.add(target) + } + } + + emit(BulkSyncProgress(total = targets.size, done = skippedCount, failed = 0)) val total = targets.size val semaphore = Semaphore(PARALLELISM) val jobs = coroutineScope { - targets.map { target -> + nonSkipped.map { target -> async { semaphore.withPermit { try { @@ -102,7 +111,7 @@ class TeacherScheduleRepositoryImpl( } } - var done = 0 + var done = skippedCount var failed = 0 for (job in jobs) { if (job.await()) done++ else failed++ diff --git a/app/src/main/java/agzam/vyatsu/domain/repository/TeacherScheduleRepository.kt b/app/src/main/java/agzam/vyatsu/domain/repository/TeacherScheduleRepository.kt index b2e407c..a109d12 100644 --- a/app/src/main/java/agzam/vyatsu/domain/repository/TeacherScheduleRepository.kt +++ b/app/src/main/java/agzam/vyatsu/domain/repository/TeacherScheduleRepository.kt @@ -10,7 +10,7 @@ import java.time.LocalDate interface TeacherScheduleRepository { fun observeTeachers(period: SchedulePeriod): Flow> fun observeSchedule(period: SchedulePeriod): Flow - suspend fun syncPeriod(date: LocalDate): Flow + suspend fun syncPeriod(date: LocalDate, force: Boolean = false): Flow suspend fun hasData(period: SchedulePeriod): Boolean suspend fun weeksFor(date: LocalDate): List suspend fun clearAll() diff --git a/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt b/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt index 72860bb..6a1ba6d 100644 --- a/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt +++ b/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt @@ -146,7 +146,15 @@ class PickerViewModel( syncJob = viewModelScope.launch { try { teacherScheduleRepository.syncPeriod(today).collect { progress -> - _uiState.value = _uiState.value.copy(syncProgress = progress) + _uiState.value = _uiState.value.copy( + syncProgress = progress, + error = if (progress.failed > 0) { + "Не удалось загрузить расписание: ${progress.failed} из " + + "${progress.total} кафедр. Проверьте подключение к интернету." + } else { + _uiState.value.error + } + ) } } catch (e: CancellationException) { throw e diff --git a/app/src/main/java/agzam/vyatsu/ui/teacher/TeacherScheduleViewModel.kt b/app/src/main/java/agzam/vyatsu/ui/teacher/TeacherScheduleViewModel.kt index 6eeac61..8a56052 100644 --- a/app/src/main/java/agzam/vyatsu/ui/teacher/TeacherScheduleViewModel.kt +++ b/app/src/main/java/agzam/vyatsu/ui/teacher/TeacherScheduleViewModel.kt @@ -89,7 +89,9 @@ class TeacherScheduleViewModel( if (!scheduleRepository.hasData(period)) { _uiState.value = _uiState.value.copy(isLoading = true, loadError = false) try { - scheduleRepository.syncPeriod(period.weekStart).collect { } + // force = true: hasData() already proved there is nothing cached for + // this period, so never let the staleness window skip the fetch. + scheduleRepository.syncPeriod(period.weekStart, force = true).collect { } } catch (e: CancellationException) { throw e } catch (e: Exception) { @@ -199,7 +201,7 @@ class TeacherScheduleViewModel( try { _uiState.value = _uiState.value.copy(isLoading = true, loadError = false) try { - scheduleRepository.syncPeriod(ScheduleDates.moscowToday()).collect { } + scheduleRepository.syncPeriod(ScheduleDates.moscowToday(), force = true).collect { } } catch (e: CancellationException) { throw e } catch (e: Exception) { diff --git a/app/src/main/res/raw/globalsign_root_r46.pem b/app/src/main/res/raw/globalsign_root_r46.pem new file mode 100644 index 0000000..b936437 --- /dev/null +++ b/app/src/main/res/raw/globalsign_root_r46.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUAMEYxCzAJBgNV +BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJv +b3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAX +BgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08Es +CVeJOaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQGvGIFAha/ +r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud316HCkD7rRlr+/fKYIje +2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo0q3v84RLHIf8E6M6cqJaESvWJ3En7YEt +bWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSEy132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvj +K8Cd+RTyG/FWaha/LIWFzXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD4 +12lPFzYE+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCNI/on +ccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzsx2sZy/N78CsHpdls +eVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqaByFrgY/bxFn63iLABJzjqls2k+g9 +vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEM +BQADggIBAHx47PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti2kM3S+LGteWy +gxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIkpnnpHs6i58FZFZ8d4kuaPp92 +CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRFFRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZm +OUdkLG5NrmJ7v2B0GbhWrJKsFjLtrWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qq +JZ4d16GLuc1CLgSkZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwye +qiv5u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP4vkYxboz +nxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6N3ec592kD3ZDZopD8p/7 +DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3vouXsXgxT7PntgMTzlSdriVZzH81Xwj3 +QEUxeCp6 +-----END CERTIFICATE----- \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..eff96c2 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,25 @@ + + + + + + + + false + + + vyatsu.ru + + + + + +