Окончательный, по моему мнению, фикс запуска постоянного уведомления.

Добавлена возможность просмотра расписания конкретного преподавателя.
This commit is contained in:
2024-11-03 07:18:33 +04:00
parent 637a66a647
commit c9bb2b6b1e
31 changed files with 768 additions and 178 deletions

View File

@@ -33,8 +33,8 @@ android {
applicationId = "ru.n08i40k.polytechnic.next"
minSdk = 26
targetSdk = 35
versionCode = 19
versionName = "2.0.3"
versionCode = 20
versionName = "2.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -128,6 +128,9 @@ dependencies {
implementation(libs.androidx.navigation.compose)
testImplementation(libs.junit)
testImplementation(libs.mockito.kotlin)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))

View File

@@ -1,8 +1,10 @@
package ru.n08i40k.polytechnic.next.data.schedule
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
interface ScheduleRepository {
suspend fun getGroup(): MyResult<Group>
suspend fun getGroup(): MyResult<GroupOrTeacher>
suspend fun getTeacher(name: String): MyResult<GroupOrTeacher>
}

View File

@@ -11,7 +11,7 @@ import kotlinx.datetime.toInstant
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.model.Day
import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.model.LessonTime
import ru.n08i40k.polytechnic.next.model.LessonType
@@ -31,14 +31,15 @@ private fun genBreak(start: Instant, end: Instant): Lesson {
start,
end
),
subGroups = listOf()
subGroups = listOf(),
group = null
)
}
class FakeScheduleRepository : ScheduleRepository {
@Suppress("SpellCheckingInspection")
companion object {
val exampleGroup = Group(
val exampleGroup = GroupOrTeacher(
name = "ИС-214/23", days = arrayListOf(
Day(
name = "Понедельник",
@@ -52,7 +53,8 @@ class FakeScheduleRepository : ScheduleRepository {
genLocalDateTime(8, 30),
genLocalDateTime(8, 40),
),
subGroups = listOf()
subGroups = listOf(),
group = null
),
genBreak(
genLocalDateTime(8, 40),
@@ -66,7 +68,8 @@ class FakeScheduleRepository : ScheduleRepository {
genLocalDateTime(8, 45),
genLocalDateTime(9, 15),
),
subGroups = listOf()
subGroups = listOf(),
group = null
),
genBreak(
genLocalDateTime(9, 15),
@@ -86,7 +89,8 @@ class FakeScheduleRepository : ScheduleRepository {
number = 1,
cabinet = "43"
)
)
),
group = null
),
genBreak(
genLocalDateTime(10, 45),
@@ -111,7 +115,8 @@ class FakeScheduleRepository : ScheduleRepository {
number = 2,
cabinet = "44"
),
)
),
group = null
),
genBreak(
genLocalDateTime(12, 15),
@@ -136,7 +141,118 @@ class FakeScheduleRepository : ScheduleRepository {
number = 2,
cabinet = "41"
),
)
),
group = null
),
)
)
)
)
val exampleTeacher = GroupOrTeacher(
name = "Хомченко Н.Е.", days = arrayListOf(
Day(
name = "Понедельник",
date = LocalDateTime.now().toInstant(TimeZone.currentSystemDefault()),
lessons = listOf(
Lesson(
type = LessonType.ADDITIONAL,
defaultRange = null,
name = "Линейка",
time = LessonTime(
genLocalDateTime(8, 30),
genLocalDateTime(8, 40),
),
subGroups = listOf(),
group = "ИС-214/23"
),
genBreak(
genLocalDateTime(8, 40),
genLocalDateTime(8, 45),
),
Lesson(
type = LessonType.ADDITIONAL,
defaultRange = null,
name = "Разговор о важном",
time = LessonTime(
genLocalDateTime(8, 45),
genLocalDateTime(9, 15),
),
subGroups = listOf(),
group = "ИС-214/23"
),
genBreak(
genLocalDateTime(9, 15),
genLocalDateTime(9, 25),
),
Lesson(
type = LessonType.DEFAULT,
defaultRange = listOf(1, 1),
name = "МДК.05.01 Проектирование и дизайн информационных систем",
time = LessonTime(
genLocalDateTime(9, 25),
genLocalDateTime(10, 45),
),
subGroups = listOf(
SubGroup(
teacher = "Ивашова А.Н.",
number = 1,
cabinet = "43"
)
),
group = "ИС-214/23"
),
genBreak(
genLocalDateTime(10, 45),
genLocalDateTime(10, 55),
),
Lesson(
type = LessonType.DEFAULT,
defaultRange = listOf(2, 2),
name = "Основы проектирования баз данных",
time = LessonTime(
genLocalDateTime(10, 55),
genLocalDateTime(12, 15),
),
subGroups = listOf(
SubGroup(
teacher = "Чинарева Е.А.",
number = 1,
cabinet = "21"
),
SubGroup(
teacher = "Ивашова А.Н.",
number = 2,
cabinet = "44"
),
),
group = "ИС-214/23"
),
genBreak(
genLocalDateTime(12, 15),
genLocalDateTime(12, 35),
),
Lesson(
type = LessonType.DEFAULT,
defaultRange = listOf(3, 3),
name = "Операционные системы и среды",
time = LessonTime(
genLocalDateTime(12, 35),
genLocalDateTime(13, 55),
),
subGroups = listOf(
SubGroup(
teacher = "Сергачева А.О.",
number = 1,
cabinet = "42"
),
SubGroup(
teacher = "Воронцева Н.В.",
number = 2,
cabinet = "41"
),
),
group = "ИС-214/23"
),
)
)
@@ -144,11 +260,12 @@ class FakeScheduleRepository : ScheduleRepository {
)
}
private val group = MutableStateFlow<Group?>(exampleGroup)
private val group = MutableStateFlow<GroupOrTeacher?>(exampleGroup)
private val teacher = MutableStateFlow<GroupOrTeacher?>(exampleTeacher)
private var updateCounter: Int = 0
override suspend fun getGroup(): MyResult<Group> {
override suspend fun getGroup(): MyResult<GroupOrTeacher> {
return withContext(Dispatchers.IO) {
delay(1500)
if (updateCounter++ % 3 == 0) MyResult.Failure(
@@ -157,4 +274,14 @@ class FakeScheduleRepository : ScheduleRepository {
else MyResult.Success(group.value!!)
}
}
override suspend fun getTeacher(name: String): MyResult<GroupOrTeacher> {
return withContext(Dispatchers.IO) {
delay(1500)
if (updateCounter++ % 3 == 0) MyResult.Failure(
IllegalStateException()
)
else MyResult.Success(teacher.value!!)
}
}
}

View File

@@ -5,12 +5,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGet
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetTeacher
import ru.n08i40k.polytechnic.next.network.tryFuture
class RemoteScheduleRepository(private val context: Context) : ScheduleRepository {
override suspend fun getGroup(): MyResult<Group> =
override suspend fun getGroup(): MyResult<GroupOrTeacher> =
withContext(Dispatchers.IO) {
val response = tryFuture {
ScheduleGet(
@@ -25,4 +26,21 @@ class RemoteScheduleRepository(private val context: Context) : ScheduleRepositor
is MyResult.Success -> MyResult.Success(response.data.group)
}
}
override suspend fun getTeacher(name: String): MyResult<GroupOrTeacher> =
withContext(Dispatchers.IO) {
val response = tryFuture {
ScheduleGetTeacher(
context,
name,
it,
it
)
}
when (response) {
is MyResult.Failure -> response
is MyResult.Success -> MyResult.Success(response.data.teacher)
}
}
}

View File

@@ -15,11 +15,13 @@ import ru.n08i40k.polytechnic.next.utils.now
@Parcelize
@Suppress("unused", "MemberVisibilityCanBePrivate")
@Serializable
class Day(
data class Day(
val name: String,
@Serializable(with = InstantAsLongSerializer::class)
@SerialName("date")
private val dateMillis: Long,
val lessons: List<Lesson>
) : Parcelable {
constructor(name: String, date: Instant, lessons: List<Lesson>) : this(
@@ -29,30 +31,19 @@ class Day(
val date: Instant
get() = Instant.fromEpochMilliseconds(dateMillis)
fun distanceToNextByLocalDateTime(from: LocalDateTime): Pair<Int, Int>? {
val toIdx = lessons
.map { it.time.start }
.indexOfFirst { it.dateTime > from }
fun distanceToNext(from: LocalDateTime): Pair<Int, Int>? {
val nextIndex = lessons.map { it.time.start }.indexOfFirst { it.dateTime >= from }
if (toIdx == -1)
return null
if (nextIndex == -1) return null
return Pair(toIdx, lessons[toIdx].time.start.dayMinutes - from.dayMinutes)
return Pair(nextIndex, lessons[nextIndex].time.start.dayMinutes - from.dayMinutes)
}
fun distanceToNextByIdx(from: Int? = null): Pair<Int, Int>? {
val fromLesson = if (from != null) lessons[from] else null
fun distanceToNext(fromIndex: Int? = null): Pair<Int, Int>? {
val fromLesson = fromIndex?.let { lessons[fromIndex] }
val fromTime = fromLesson?.time?.end?.dateTime ?: LocalDateTime.now()
if (from != null && fromLesson == null)
throw NullPointerException("Lesson (by given index) and it's time should be non-null!")
val fromTime =
if (from != null)
fromLesson!!.time.end.dateTime
else
LocalDateTime.now()
return distanceToNextByLocalDateTime(fromTime)
return distanceToNext(fromTime)
}
// current
@@ -63,8 +54,7 @@ class Day(
for (lessonIdx in lessons.indices) {
val lesson = lessons[lessonIdx]
if (lesson.time.start.dateTime <= now && now < lesson.time.end.dateTime)
return lessonIdx
if (lesson.time.start.dateTime <= now && now < lesson.time.end.dateTime) return lessonIdx
}
return null

View File

@@ -8,7 +8,7 @@ import java.util.Calendar
@Suppress("MemberVisibilityCanBePrivate")
@Parcelize
@Serializable
data class Group(
data class GroupOrTeacher(
val name: String,
val days: List<Day>
) : Parcelable {

View File

@@ -15,6 +15,7 @@ data class Lesson(
val defaultRange: List<Int>?,
val name: String?,
val time: LessonTime,
val group: String? = null,
val subGroups: List<SubGroup>
) : Parcelable {
val duration: Int
@@ -26,6 +27,10 @@ data class Lesson(
}
fun getNameAndCabinetsShort(context: Context): String {
val name =
if (type == LessonType.BREAK) context.getString(R.string.lesson_break)
else this.name
val limitedName = name!! limit 15
val cabinets = subGroups.map { it.cabinet }

View File

@@ -4,7 +4,7 @@ import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
class ScheduleGet(
@@ -18,13 +18,10 @@ class ScheduleGet(
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
@Serializable
data class RequestDto(val name: String)
@Serializable
data class ResponseDto(
val updatedAt: String,
val group: Group,
val group: GroupOrTeacher,
val updated: ArrayList<Int>,
)
}

View File

@@ -0,0 +1,28 @@
package ru.n08i40k.polytechnic.next.network.request.schedule
import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
class ScheduleGetTeacher(
context: Context,
teacher: String,
listener: Response.Listener<ResponseDto>,
errorListener: Response.ErrorListener? = null
) : CachedRequest(
context,
Method.GET,
"v2/schedule/teacher/$teacher",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
@Serializable
data class ResponseDto(
val updatedAt: String,
val teacher: GroupOrTeacher,
val updated: ArrayList<Int>,
)
}

View File

@@ -0,0 +1,24 @@
package ru.n08i40k.polytechnic.next.network.request.schedule
import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.RequestBase
class ScheduleGetTeacherNames(
context: Context,
listener: Response.Listener<ResponseDto>,
errorListener: Response.ErrorListener? = null
) : RequestBase(
context,
Method.GET,
"v2/schedule/teacher-names",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
@Serializable
data class ResponseDto(
val names: ArrayList<String>,
)
}

View File

@@ -16,20 +16,19 @@ import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.model.Day
import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import ru.n08i40k.polytechnic.next.utils.dayMinutes
import ru.n08i40k.polytechnic.next.utils.fmtAsClock
import ru.n08i40k.polytechnic.next.utils.getDayMinutes
import ru.n08i40k.polytechnic.next.utils.now
import java.util.Calendar
import java.util.logging.Logger
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
class CurrentLessonViewService : Service() {
companion object {
private const val NOTIFICATION_STATUS_ID = 1337
private const val NOTIFICATION_END_ID = NOTIFICATION_STATUS_ID + 1
private const val UPDATE_INTERVAL = 60_000L
private const val UPDATE_INTERVAL = 1_000L
suspend fun startService(application: PolytechnicApplication) {
if (!application.hasNotificationPermission())
@@ -59,61 +58,32 @@ class CurrentLessonViewService : Service() {
}
}
private var day: Day? = null
private lateinit var day: Day
private val handler = Handler(Looper.getMainLooper())
private val updateRunnable = object : Runnable {
override fun run() {
val logger = Logger.getLogger("CLV.updateRunnable")
val (currentIndex, currentLesson) = day.currentKV ?: (null to null)
val (nextIndex, _) = day.distanceToNext(currentIndex)
?: (null to null)
if (day == null || day!!.lessons.isEmpty()) {
logger.warning("Stopping, because day is null or empty!")
stopSelf()
return
}
val currentMinutes = Calendar.getInstance()
.get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance()
.get(Calendar.MINUTE)
val currentLessonEntry = day!!.currentKV
val currentLessonIdx: Int? = currentLessonEntry?.first
val currentLesson: Lesson? = currentLessonEntry?.second
val nextLessonEntry = day!!.distanceToNextByIdx(currentLessonIdx)
val nextLesson =
if (nextLessonEntry == null)
null
else
day!!.lessons[nextLessonEntry.first]
val nextLesson = nextIndex?.let { day.lessons[nextIndex] }
if (currentLesson == null && nextLesson == null) {
val notification = NotificationCompat
.Builder(applicationContext, NotificationChannels.LESSON_VIEW)
.setSmallIcon(R.drawable.schedule)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentTitle(getString(R.string.lessons_end_notification_title))
.setContentText(getString(R.string.lessons_end_notification_description))
.build()
getNotificationManager().notify(NOTIFICATION_END_ID, notification)
stopSelf()
onLessonsEnd()
return
}
val firstLessonIdx =
day!!.distanceToNextByLocalDateTime(LocalDateTime(1, 1, 1, 0, 0))?.first
?: throw NullPointerException("Is this even real?")
val distanceToFirst = day!!.lessons[firstLessonIdx]!!.time!!.start.dayMinutes - currentMinutes
handler.postDelayed(this, UPDATE_INTERVAL)
val currentLessonDelay =
if (currentLesson == null) // Если эта пара - перемена, то конец перемены через (результат getDistanceToNext)
nextLessonEntry!!.second
else // Если эта пара - обычная пара, то конец пары через (конец этой пары - текущее кол-во минут)
currentLesson.time!!.end.dayMinutes - currentMinutes
val context = this@CurrentLessonViewService
val currentMinutes = LocalDateTime.now().dayMinutes
val distanceToFirst = day.first!!.time.start.dayMinutes - currentMinutes
val currentLessonName =
currentLesson?.getNameAndCabinetsShort(this@CurrentLessonViewService)
currentLesson?.getNameAndCabinetsShort(context)
?: run {
if (distanceToFirst > 0)
getString(R.string.lessons_not_started)
@@ -122,41 +92,42 @@ class CurrentLessonViewService : Service() {
}
val nextLessonName =
if (currentLesson == null) // Если текущая пара - перемена
nextLesson!!.getNameAndCabinetsShort(this@CurrentLessonViewService)
else if (nextLesson == null) // Если текущая пара - последняя
getString(R.string.lessons_end)
else // Если после текущей пары есть ещё пара(ы)
getString(R.string.lesson_break)
nextLesson?.getNameAndCabinetsShort(context) ?: getString(R.string.lessons_end)
val nextLessonTotal =
if (currentLesson == null)
nextLesson!!.time!!.start
else
currentLesson.time!!.end
val nextLessonIn =
(currentLesson?.time?.end ?: nextLesson!!.time.start).dayMinutes
val notification = createNotification(
getString(
if (distanceToFirst > 0)
R.string.waiting_for_day_start_notification_title
else
R.string.lesson_going_notification_title,
currentLessonDelay / 60,
currentLessonDelay % 60
if (distanceToFirst > 0) R.string.waiting_for_day_start_notification_title
else R.string.lesson_going_notification_title,
(nextLessonIn - currentMinutes) / 60,
(nextLessonIn - currentMinutes) % 60
),
getString(
R.string.lesson_going_notification_description,
currentLessonName,
nextLessonTotal.dayMinutes.fmtAsClock(),
nextLessonIn.fmtAsClock(),
nextLessonName,
)
)
getNotificationManager().notify(NOTIFICATION_STATUS_ID, notification)
handler.postDelayed(this, UPDATE_INTERVAL)
}
}
private fun onLessonsEnd() {
val notification = NotificationCompat
.Builder(applicationContext, NotificationChannels.LESSON_VIEW)
.setSmallIcon(R.drawable.schedule)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentTitle(getString(R.string.lessons_end_notification_title))
.setContentText(getString(R.string.lessons_end_notification_description))
.build()
getNotificationManager().notify(NOTIFICATION_END_ID, notification)
stopSelf()
}
private fun createNotification(
title: String? = null,
description: String? = null
@@ -176,7 +147,7 @@ class CurrentLessonViewService : Service() {
return getSystemService(NOTIFICATION_SERVICE) as NotificationManager
}
private fun updateSchedule(group: Group?) {
private fun updateSchedule(group: GroupOrTeacher?) {
val logger = Logger.getLogger("CLV")
if (group == null) {
@@ -223,7 +194,7 @@ class CurrentLessonViewService : Service() {
updateSchedule(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("group", Group::class.java)
intent.getParcelableExtra("group", GroupOrTeacher::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra("group")

View File

@@ -5,6 +5,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Person
import androidx.compose.ui.graphics.vector.ImageVector
import ru.n08i40k.polytechnic.next.R
@@ -19,6 +20,7 @@ object Constants {
val bottomNavItem = listOf(
BottomNavItem(R.string.profile, Icons.Filled.AccountCircle, "profile"),
BottomNavItem(R.string.replacer, Icons.Filled.Create, "replacer", true),
BottomNavItem(R.string.schedule, Icons.Filled.DateRange, "schedule")
BottomNavItem(R.string.schedule, Icons.Filled.DateRange, "schedule"),
BottomNavItem(R.string.teachers, Icons.Filled.Person, "teacher-schedule")
)
}

View File

@@ -70,12 +70,14 @@ import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Download
import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Telegram
import ru.n08i40k.polytechnic.next.ui.main.profile.ProfileScreen
import ru.n08i40k.polytechnic.next.ui.main.replacer.ReplacerScreen
import ru.n08i40k.polytechnic.next.ui.main.schedule.ScheduleScreen
import ru.n08i40k.polytechnic.next.ui.main.schedule.group.GroupScheduleScreen
import ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.TeacherScheduleScreen
import ru.n08i40k.polytechnic.next.ui.model.GroupScheduleViewModel
import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel
import ru.n08i40k.polytechnic.next.ui.model.RemoteConfigViewModel
import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerViewModel
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
import ru.n08i40k.polytechnic.next.ui.model.TeacherScheduleViewModel
import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
@@ -83,7 +85,8 @@ import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
private fun NavHostContainer(
navController: NavHostController,
padding: PaddingValues,
scheduleViewModel: ScheduleViewModel,
groupScheduleViewModel: GroupScheduleViewModel,
teacherScheduleViewModel: TeacherScheduleViewModel,
scheduleReplacerViewModel: ScheduleReplacerViewModel?
) {
val context = LocalContext.current
@@ -121,7 +124,15 @@ private fun NavHostContainer(
}
composable("schedule") {
ScheduleScreen(scheduleViewModel) { scheduleViewModel.refreshGroup() }
GroupScheduleScreen(groupScheduleViewModel) { groupScheduleViewModel.refresh() }
}
composable("teacher-schedule") {
TeacherScheduleScreen(teacherScheduleViewModel) {
if (it.isNotEmpty()) teacherScheduleViewModel.fetch(
it
)
}
}
if (scheduleReplacerViewModel != null) {
@@ -283,8 +294,12 @@ fun MainScreen(
)
// schedule view model
val scheduleViewModel =
hiltViewModel<ScheduleViewModel>(LocalContext.current as ComponentActivity)
val groupScheduleViewModel =
hiltViewModel<GroupScheduleViewModel>(LocalContext.current as ComponentActivity)
// teacher view model
val teacherScheduleViewModel =
hiltViewModel<TeacherScheduleViewModel>(LocalContext.current as ComponentActivity)
// schedule replacer view model
val profileUiState by profileViewModel.uiState.collectAsStateWithLifecycle()
@@ -312,7 +327,8 @@ fun MainScreen(
NavHostContainer(
navController,
paddingValues,
scheduleViewModel,
groupScheduleViewModel,
teacherScheduleViewModel,
scheduleReplacerViewModel
)
}

View File

@@ -38,7 +38,7 @@ import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
import ru.n08i40k.polytechnic.next.ui.model.GroupScheduleViewModel
import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
@Preview(showBackground = true)
@@ -176,8 +176,8 @@ internal fun ProfileCard(profile: Profile = FakeProfileRepository.exampleProfile
}
if (groupChanging) {
val scheduleViewModel =
hiltViewModel<ScheduleViewModel>(LocalContext.current as ComponentActivity)
val groupScheduleViewModel =
hiltViewModel<GroupScheduleViewModel>(LocalContext.current as ComponentActivity)
ChangeGroupDialog(
context,
@@ -194,7 +194,7 @@ internal fun ProfileCard(profile: Profile = FakeProfileRepository.exampleProfile
.clear()
}
context.profileViewModel!!.refreshProfile {
scheduleViewModel.refreshGroup()
groupScheduleViewModel.refresh()
}
}
) { groupChanging = false }

View File

@@ -102,7 +102,7 @@ fun DayCard(
text = stringResource(when (distance) {
-1 -> R.string.yesterday
0 -> R.string.today
1 -> R.string.tommorow
1 -> R.string.tomorrow
else -> throw RuntimeException()
}),
)

View File

@@ -17,7 +17,7 @@ import androidx.compose.ui.util.lerp
import kotlinx.datetime.LocalDateTime
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import ru.n08i40k.polytechnic.next.ui.widgets.NotificationCard
import ru.n08i40k.polytechnic.next.utils.dateTime
import ru.n08i40k.polytechnic.next.utils.now
@@ -25,9 +25,9 @@ import java.util.Calendar
import java.util.logging.Level
import kotlin.math.absoluteValue
private fun isScheduleOutdated(group: Group): Boolean {
private fun isScheduleOutdated(groupOrTeacher: GroupOrTeacher): Boolean {
val nowDateTime = LocalDateTime.now()
val lastDay = group.days.lastOrNull() ?: return true
val lastDay = groupOrTeacher.days.lastOrNull() ?: return true
val lastLesson = lastDay.last ?: return true
return nowDateTime > lastLesson.time.end.dateTime
@@ -35,17 +35,17 @@ private fun isScheduleOutdated(group: Group): Boolean {
@Preview
@Composable
fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
fun DayPager(groupOrTeacher: GroupOrTeacher = FakeScheduleRepository.exampleGroup) {
val currentDay = (Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 2)
val calendarDay = if (currentDay == -1) 6 else currentDay
val pagerState = rememberPagerState(
initialPage = calendarDay
.coerceAtMost(group.days.size - 1),
pageCount = { group.days.size })
.coerceAtMost(groupOrTeacher.days.size - 1),
pageCount = { groupOrTeacher.days.size })
Column {
if (isScheduleOutdated(group)) {
if (isScheduleOutdated(groupOrTeacher)) {
NotificationCard(
level = Level.WARNING,
title = stringResource(R.string.outdated_schedule)
@@ -73,7 +73,7 @@ fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
start = 0.5f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f)
)
},
day = group.days[page],
day = groupOrTeacher.days[page],
distance = page - currentDay
)
}

View File

@@ -117,6 +117,7 @@ private fun LessonViewRow(
timeFormat: LessonTimeFormat = LessonTimeFormat.FROM_TO,
name: String = "Test",
subGroups: List<SubGroup> = listOf(),
group: String? = "ИС-214/23",
cardColors: CardColors = CardDefaults.cardColors(),
verticalPadding: Dp = 10.dp,
now: Boolean = true,
@@ -201,6 +202,16 @@ private fun LessonViewRow(
color = contentColor
)
if (group != null) {
Text(
text = group,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = contentColor
)
}
for (subGroup in subGroups) {
Text(
text = subGroup.teacher,
@@ -212,8 +223,10 @@ private fun LessonViewRow(
}
Column(modifier = Modifier.wrapContentWidth()) {
if (subGroups.size != 1)
Text(text = "")
if (subGroups.size != 1) {
for (i in 0..<(if (group != null) 2 else 1))
Text(text = "")
}
for (subGroup in subGroups) {
Text(
text = subGroup.cabinet,
@@ -244,6 +257,7 @@ fun FreeLessonRow(
LessonTimeFormat.ONLY_MINUTES_DURATION,
stringResource(R.string.lesson_break),
lesson.subGroups,
lesson.group,
cardColors,
2.5.dp,
now
@@ -264,6 +278,7 @@ fun LessonRow(
LessonTimeFormat.FROM_TO,
lesson.name!!,
lesson.subGroups,
lesson.group,
cardColors,
5.dp,
now

View File

@@ -1,4 +1,4 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule
package ru.n08i40k.polytechnic.next.ui.main.schedule.group
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -26,9 +26,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.delay
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MockAppContainer
import ru.n08i40k.polytechnic.next.ui.main.schedule.DayPager
import ru.n08i40k.polytechnic.next.ui.model.GroupScheduleUiState
import ru.n08i40k.polytechnic.next.ui.model.GroupScheduleViewModel
import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
import ru.n08i40k.polytechnic.next.ui.model.ScheduleUiState
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
@Composable
private fun rememberUpdatedLifecycleOwner(): LifecycleOwner {
@@ -38,11 +39,11 @@ private fun rememberUpdatedLifecycleOwner(): LifecycleOwner {
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun ScheduleScreen(
scheduleViewModel: ScheduleViewModel = ScheduleViewModel(MockAppContainer(LocalContext.current)),
fun GroupScheduleScreen(
groupScheduleViewModel: GroupScheduleViewModel = GroupScheduleViewModel(MockAppContainer(LocalContext.current)),
onRefresh: () -> Unit = {}
) {
val uiState by scheduleViewModel.uiState.collectAsStateWithLifecycle()
val uiState by groupScheduleViewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(uiState) {
delay(120_000)
onRefresh()
@@ -69,25 +70,25 @@ fun ScheduleScreen(
LoadingContent(
empty = when (uiState) {
is ScheduleUiState.NoSchedule -> uiState.isLoading
is ScheduleUiState.HasSchedule -> false
is GroupScheduleUiState.NoData -> uiState.isLoading
is GroupScheduleUiState.HasData -> false
},
loading = uiState.isLoading,
onRefresh = onRefresh,
verticalArrangement = Arrangement.Top
) {
when (uiState) {
is ScheduleUiState.HasSchedule -> {
is GroupScheduleUiState.HasData -> {
Column {
val hasSchedule = uiState as ScheduleUiState.HasSchedule
val hasData = uiState as GroupScheduleUiState.HasData
UpdateInfo(hasSchedule.lastUpdateAt, hasSchedule.updateDates)
UpdateInfo(hasData.lastUpdateAt, hasData.updateDates)
Spacer(Modifier.height(10.dp))
DayPager(hasSchedule.group)
DayPager(hasData.group)
}
}
is ScheduleUiState.NoSchedule -> {
is GroupScheduleUiState.NoData -> {
if (!uiState.isLoading) {
TextButton(onClick = onRefresh, modifier = Modifier.fillMaxSize()) {
Text(stringResource(R.string.reload), textAlign = TextAlign.Center)

View File

@@ -1,4 +1,4 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule
package ru.n08i40k.polytechnic.next.ui.main.schedule.group
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable

View File

@@ -1,4 +1,4 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule
package ru.n08i40k.polytechnic.next.ui.main.schedule.group
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement

View File

@@ -0,0 +1,90 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBox(
title: String,
onSearchAttempt: (String) -> Unit,
variants: List<String>,
) {
var value by remember { mutableStateOf("") }
val searchableVariants =
remember(variants.size) { variants.map { it.replace(" ", "").replace(".", "").lowercase() } }
val filteredVariants = remember(searchableVariants, value) {
searchableVariants.filter { it.contains(value) }
}
var dropdownExpanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = dropdownExpanded,
onExpandedChange = {}
) {
Row(modifier = Modifier.fillMaxWidth()) {
TextField(
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
if (it.hasFocus)
dropdownExpanded = true
}
.menuAnchor(MenuAnchorType.PrimaryEditable, true),
label = { Text(title) },
value = value,
onValueChange = {
value = it
dropdownExpanded = true
},
trailingIcon = {
IconButton(onClick = { onSearchAttempt(value) }) {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = "Search"
)
}
},
singleLine = true,
)
}
ExposedDropdownMenu(
expanded = dropdownExpanded,
onDismissRequest = { dropdownExpanded = false }
) {
filteredVariants.forEach {
val fullVariant = variants[searchableVariants.indexOf(it)]
DropdownMenuItem(
text = { Text(fullVariant) },
onClick = {
value = fullVariant
onSearchAttempt(value)
dropdownExpanded = false
}
)
}
}
}
}

View File

@@ -0,0 +1,114 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.delay
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MockAppContainer
import ru.n08i40k.polytechnic.next.ui.main.schedule.DayPager
import ru.n08i40k.polytechnic.next.ui.model.TeacherScheduleUiState
import ru.n08i40k.polytechnic.next.ui.model.TeacherScheduleViewModel
import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
@Composable
private fun rememberUpdatedLifecycleOwner(): LifecycleOwner {
val lifecycleOwner = LocalLifecycleOwner.current
return remember { lifecycleOwner }
}
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun TeacherScheduleScreen(
teacherScheduleViewModel: TeacherScheduleViewModel = TeacherScheduleViewModel(
MockAppContainer(
LocalContext.current
)
),
fetch: (String) -> Unit = {}
) {
var teacherName by remember { mutableStateOf("") }
val uiState by teacherScheduleViewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(uiState) {
delay(120_000)
fetch(teacherName)
}
val lifecycleOwner = rememberUpdatedLifecycleOwner()
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
fetch(teacherName)
}
else -> Unit
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
Column(Modifier.fillMaxSize()) {
TeacherSearchBox(onSearchAttempt = {
teacherName = it
fetch(it)
})
Spacer(Modifier.height(10.dp))
LoadingContent(
empty = when (uiState) {
is TeacherScheduleUiState.NoData -> uiState.isLoading
is TeacherScheduleUiState.HasData -> false
},
loading = uiState.isLoading,
) {
when (uiState) {
is TeacherScheduleUiState.HasData -> {
Column {
val hasData = uiState as TeacherScheduleUiState.HasData
DayPager(hasData.teacher)
}
}
is TeacherScheduleUiState.NoData -> {
if (!uiState.isLoading) {
Text(
modifier = Modifier.fillMaxSize(),
text = stringResource(R.string.teacher_not_selected),
textAlign = TextAlign.Center
)
}
}
}
}
}
}

View File

@@ -0,0 +1,46 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetTeacherNames
@Composable
private fun getTeacherNames(context: Context): ArrayList<String> {
val teacherNames = remember { arrayListOf<String>() }
LaunchedEffect(teacherNames) {
ScheduleGetTeacherNames(context, {
teacherNames.clear()
teacherNames.addAll(it.names)
}, {
teacherNames.clear()
}).send()
}
return teacherNames
}
@Preview(showBackground = true)
@Composable
fun TeacherSearchBox(
onSearchAttempt: (String) -> Unit = {},
) {
val teachers = getTeacherNames(LocalContext.current)
val focusManager = LocalFocusManager.current
SearchBox(
stringResource(R.string.teacher_name),
{
focusManager.clearFocus(true)
onSearchAttempt(it)
},
teachers,
)
}

View File

@@ -12,55 +12,55 @@ import kotlinx.coroutines.launch
import ru.n08i40k.polytechnic.next.UpdateDates
import ru.n08i40k.polytechnic.next.data.AppContainer
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import java.util.Date
import javax.inject.Inject
sealed interface ScheduleUiState {
sealed interface GroupScheduleUiState {
val isLoading: Boolean
data class NoSchedule(
data class NoData(
override val isLoading: Boolean
) : ScheduleUiState
) : GroupScheduleUiState
data class HasSchedule(
val group: Group,
data class HasData(
val group: GroupOrTeacher,
val updateDates: UpdateDates,
val lastUpdateAt: Long,
override val isLoading: Boolean
) : ScheduleUiState
) : GroupScheduleUiState
}
private data class ScheduleViewModelState(
val group: Group? = null,
private data class GroupScheduleViewModelState(
val group: GroupOrTeacher? = null,
val updateDates: UpdateDates? = null,
val lastUpdateAt: Long = 0,
val isLoading: Boolean = false
) {
fun toUiState(): ScheduleUiState = if (group == null) {
ScheduleUiState.NoSchedule(isLoading)
fun toUiState(): GroupScheduleUiState = if (group == null) {
GroupScheduleUiState.NoData(isLoading)
} else {
ScheduleUiState.HasSchedule(group, updateDates!!, lastUpdateAt, isLoading)
GroupScheduleUiState.HasData(group, updateDates!!, lastUpdateAt, isLoading)
}
}
@HiltViewModel
class ScheduleViewModel @Inject constructor(
class GroupScheduleViewModel @Inject constructor(
appContainer: AppContainer
) : ViewModel() {
private val scheduleRepository = appContainer.scheduleRepository
private val networkCacheRepository = appContainer.networkCacheRepository
private val viewModelState = MutableStateFlow(ScheduleViewModelState(isLoading = true))
private val viewModelState = MutableStateFlow(GroupScheduleViewModelState(isLoading = true))
val uiState = viewModelState
.map(ScheduleViewModelState::toUiState)
.map(GroupScheduleViewModelState::toUiState)
.stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState())
init {
refreshGroup()
refresh()
}
fun refreshGroup() {
fun refresh() {
viewModelState.update { it.copy(isLoading = true) }
viewModelScope.launch {

View File

@@ -0,0 +1,100 @@
package ru.n08i40k.polytechnic.next.ui.model
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.n08i40k.polytechnic.next.UpdateDates
import ru.n08i40k.polytechnic.next.data.AppContainer
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import java.util.Date
import javax.inject.Inject
sealed interface TeacherScheduleUiState {
val isLoading: Boolean
data class NoData(
override val isLoading: Boolean
) : TeacherScheduleUiState
data class HasData(
val teacher: GroupOrTeacher,
val updateDates: UpdateDates,
val lastUpdateAt: Long,
override val isLoading: Boolean
) : TeacherScheduleUiState
}
private data class TeacherScheduleViewModelState(
val teacher: GroupOrTeacher? = null,
val updateDates: UpdateDates? = null,
val lastUpdateAt: Long = 0,
val isLoading: Boolean = false
) {
fun toUiState(): TeacherScheduleUiState = if (teacher == null) {
TeacherScheduleUiState.NoData(isLoading)
} else {
TeacherScheduleUiState.HasData(teacher, updateDates!!, lastUpdateAt, isLoading)
}
}
@HiltViewModel
class TeacherScheduleViewModel @Inject constructor(
appContainer: AppContainer
) : ViewModel() {
private val scheduleRepository = appContainer.scheduleRepository
private val networkCacheRepository = appContainer.networkCacheRepository
private val viewModelState = MutableStateFlow(TeacherScheduleViewModelState(isLoading = true))
val uiState = viewModelState
.map(TeacherScheduleViewModelState::toUiState)
.stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState())
init {
fetch(null)
}
fun fetch(name: String?) {
if (name == null) {
viewModelState.update {
it.copy(
teacher = null,
isLoading = false
)
}
return
}
viewModelState.update { it.copy(isLoading = true) }
viewModelScope.launch {
val result = scheduleRepository.getTeacher(name)
viewModelState.update {
when (result) {
is MyResult.Success -> {
val updateDates = networkCacheRepository.getUpdateDates()
it.copy(
teacher = result.data,
updateDates = updateDates,
lastUpdateAt = Date().time,
isLoading = false
)
}
is MyResult.Failure -> it.copy(
teacher = null,
isLoading = false
)
}
}
}
}
}

View File

@@ -37,6 +37,24 @@ fun LoadingContent(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoadingContent(
empty: Boolean,
emptyContent: @Composable () -> Unit = { FullScreenLoading() },
loading: Boolean,
content: @Composable () -> Unit
) {
if (empty) {
emptyContent()
return
}
PullToRefreshBox(isRefreshing = loading, onRefresh = {}) {
content()
}
}
@Preview(showBackground = true)
@Composable
fun FullScreenLoading() {

View File

@@ -43,6 +43,9 @@ val Instant.dayMinutes: Int
val LocalDateTime.dayMinutes: Int
get() = this.hour * 60 + this.minute
fun LocalDateTime.Companion.fromEpochMilliseconds(milliseconds: Long): LocalDateTime =
Instant.fromEpochMilliseconds(milliseconds).dateTime
val Instant.dateTime: LocalDateTime
get() = this.toLocalDateTime(TimeZone.currentSystemDefault())

View File

@@ -75,5 +75,9 @@
<string name="no_connection">Нет подключения к интернету!</string>
<string name="today">Сегодня</string>
<string name="yesterday">Вчера</string>
<string name="tommorow">Завтра</string>
<string name="tomorrow">Завтра</string>
<string name="teachers">Преподаватели</string>
<string name="teacher_name">ФИО преподавателя</string>
<string name="teacher_not_selected">Преподаватель не выбран или вы допустили ошибку в его ФИО.</string>
<string name="failed_to_fetch_teacher_names">Не удалось получить список ФИО преподавателей!</string>
</resources>

View File

@@ -75,5 +75,9 @@
<string name="no_connection">No internet connection!</string>
<string name="today">Today</string>
<string name="yesterday">Yesterday</string>
<string name="tommorow">Tommorow</string>
<string name="tomorrow">Tomorrow</string>
<string name="teachers">Teachers</string>
<string name="teacher_name">Teacher name</string>
<string name="teacher_not_selected">Teacher not selected or you made mistake in his name.</string>
<string name="failed_to_fetch_teacher_names">Failed to fetch teacher names!</string>
</resources>

View File

@@ -1,17 +1,27 @@
package ru.n08i40k.polytechnic.next
import android.content.Context
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(MockitoJUnitRunner.Silent::class)
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
fun getNameAndCabinetsShort_isNotThrow() {
val mockContext = mock<Context> {
on { getString(R.string.in_gym_lc) } doReturn "с"
on { getString(R.string.lesson_break) } doReturn "Перемена"
}
val group = FakeScheduleRepository.exampleGroup
for (day in group.days) {
for (lesson in day.lessons) {
lesson.getNameAndCabinetsShort(mockContext)
}
}
}
}