Отображение текущей пары в уведомлении.

Мне на данный момент невероятно сложно написать код для запуска сервиса перед началом пар (мне лень), поэтому сервис будет запускаться каждые 7 утра с задежкой до 15 минут.
This commit is contained in:
2024-10-08 16:13:35 +04:00
parent dde7f3e254
commit 3da65a3327
20 changed files with 544 additions and 52 deletions

View File

@@ -37,6 +37,10 @@
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" /> <option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" /> <option name="previewFile" value="true" />

13
.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@@ -8,6 +8,7 @@ plugins {
alias(libs.plugins.compose.compiler) alias(libs.plugins.compose.compiler)
kotlin("plugin.serialization") version "2.0.20" kotlin("plugin.serialization") version "2.0.20"
id("kotlin-parcelize")
id("com.google.devtools.ksp") id("com.google.devtools.ksp")
@@ -32,8 +33,8 @@ android {
applicationId = "ru.n08i40k.polytechnic.next" applicationId = "ru.n08i40k.polytechnic.next"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 11 versionCode = 12
versionName = "1.6.0" versionName = "1.7.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@@ -4,6 +4,8 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<application <application
android:name=".PolytechnicApplication" android:name=".PolytechnicApplication"
@@ -24,6 +26,15 @@
</intent-filter> </intent-filter>
</service> </service>
<service
android:name=".service.CurrentLessonViewService"
android:exported="false"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Service for viewing current lesson in notification." />
</service>
<activity <activity
android:name=".ui.MainActivity" android:name=".ui.MainActivity"
android:exported="true" android:exported="true"

View File

@@ -1,6 +1,7 @@
package ru.n08i40k.polytechnic.next package ru.n08i40k.polytechnic.next
object NotificationChannels { object NotificationChannels {
const val LESSON_VIEW = "lesson-view"
const val SCHEDULE_UPDATE = "schedule-update" const val SCHEDULE_UPDATE = "schedule-update"
const val APP_UPDATE = "app-update" const val APP_UPDATE = "app-update"
} }

View File

@@ -1,6 +1,10 @@
package ru.n08i40k.polytechnic.next package ru.n08i40k.polytechnic.next
import android.Manifest
import android.app.Application import android.app.Application
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import ru.n08i40k.polytechnic.next.data.AppContainer import ru.n08i40k.polytechnic.next.data.AppContainer
import ru.n08i40k.polytechnic.next.utils.or import ru.n08i40k.polytechnic.next.utils.or
@@ -17,4 +21,10 @@ class PolytechnicApplication : Application() {
.getPackageInfo(this.packageName, 0) .getPackageInfo(this.packageName, 0)
.versionName or "1.0.0" .versionName or "1.0.0"
} }
fun hasNotificationPermission(): Boolean {
return (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|| ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED)
}
} }

View File

@@ -1,7 +1,11 @@
package ru.n08i40k.polytechnic.next.model package ru.n08i40k.polytechnic.next.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.Calendar
@Parcelize
@Suppress("unused") @Suppress("unused")
@Serializable @Serializable
class Day( class Day(
@@ -10,4 +14,62 @@ class Day(
val defaultIndices: ArrayList<Int>, val defaultIndices: ArrayList<Int>,
val customIndices: ArrayList<Int>, val customIndices: ArrayList<Int>,
val lessons: ArrayList<Lesson?> val lessons: ArrayList<Lesson?>
) : Parcelable {
fun getDistanceToNextByMinutes(from: Int): Map.Entry<Int, Int>? {
val toIdx = lessons
.map { if (it?.time == null) null else it.time.start }
.indexOfFirst { if (it == null) false else it > from }
if (toIdx == -1)
return null
return object : Map.Entry<Int, Int> {
override val key: Int
get() = toIdx
override val value: Int
get() = lessons[toIdx]!!.time!!.start - from
}
}
fun getDistanceToNextByIdx(from: Int? = null): Map.Entry<Int, Int>? {
val fromLesson = if (from != null) lessons[from] else null
if (from != null && fromLesson?.time == null)
throw NullPointerException("Lesson (by given index) and it's time should be non-null!")
val fromTime =
if (from != null)
fromLesson!!.time!!.end
else
Calendar.getInstance()
.get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance()
.get(Calendar.MINUTE)
return getDistanceToNextByMinutes(fromTime)
}
fun getCurrentLesson(): Map.Entry<Int, Lesson>? {
val minutes = Calendar.getInstance()
.get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance()
.get(Calendar.MINUTE)
for (lessonIdx in 0..<lessons.size) {
val lesson = lessons[lessonIdx] ?: continue
if (lesson.time == null
|| minutes < lesson.time.start
|| minutes >= lesson.time.end
) )
continue
return object : Map.Entry<Int, Lesson> {
override val key: Int
get() = lessonIdx
override val value: Lesson
get() = lesson
}
}
return null
}
}

View File

@@ -1,9 +1,27 @@
package ru.n08i40k.polytechnic.next.model package ru.n08i40k.polytechnic.next.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.Calendar
@Parcelize
@Serializable @Serializable
class Group( data class Group(
val name: String, val name: String,
val days: ArrayList<Day?> val days: ArrayList<Day?>
) ) : Parcelable {
fun getCurrentDay(): Map.Entry<Int, Day?>? {
val currentDay = (Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 2)
if (currentDay < 0 || currentDay > days.size - 1)
return null
return object : Map.Entry<Int, Day?> {
override val key: Int
get() = currentDay
override val value: Day?
get() = days[currentDay]
}
}
}

View File

@@ -1,7 +1,13 @@
package ru.n08i40k.polytechnic.next.model package ru.n08i40k.polytechnic.next.model
import android.content.Context
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.utils.limit
@Parcelize
@Serializable @Serializable
data class Lesson( data class Lesson(
val type: LessonType, val type: LessonType,
@@ -10,4 +16,37 @@ data class Lesson(
val time: LessonTime?, val time: LessonTime?,
val cabinets: ArrayList<String>, val cabinets: ArrayList<String>,
val teacherNames: ArrayList<String> val teacherNames: ArrayList<String>
) : Parcelable {
fun getDuration(): Int? {
if (this.time == null)
return null
return time.end - time.start
}
fun getNameAndCabinetsShort(context: Context): String {
val limitedName = name limit 15
if (cabinets.isEmpty())
return limitedName
if (cabinets.size == 1 && cabinets[0] == "с")
return buildString {
append(limitedName)
append(" ")
append(context.getString(R.string.in_gym_lc))
}
return buildString {
append(limitedName)
append(" ")
append(
context.getString(
R.string.in_cabinets_short_lc,
cabinets.joinToString(", ")
) )
)
}
}
}

View File

@@ -1,6 +1,9 @@
package ru.n08i40k.polytechnic.next.model package ru.n08i40k.polytechnic.next.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Parcelize
@Serializable @Serializable
data class LessonTime(val start: Int, val end: Int) data class LessonTime(val start: Int, val end: Int) : Parcelable

View File

@@ -0,0 +1,222 @@
package ru.n08i40k.polytechnic.next.service
import android.app.Notification
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import androidx.core.app.NotificationCompat
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import ru.n08i40k.polytechnic.next.NotificationChannels
import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.R
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.utils.fmtAsClock
import ru.n08i40k.polytechnic.next.work.StartClvService
import java.util.Calendar
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
fun startService(appContext: Context) {
if (!(appContext as PolytechnicApplication).hasNotificationPermission())
return
if (Calendar.getInstance()
.get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance()
.get(Calendar.MINUTE) < 420)
return
val request = OneTimeWorkRequestBuilder<StartClvService>()
.build()
WorkManager.getInstance(appContext).enqueue(request)
}
}
private var day: Day? = null
private val handler = Handler(Looper.getMainLooper())
private val updateRunnable = object : Runnable {
override fun run() {
if (day == null || day!!.nonNullIndices.isEmpty()) {
stopSelf()
return
}
val currentMinutes = Calendar.getInstance()
.get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance()
.get(Calendar.MINUTE)
val currentLessonEntry = day!!.getCurrentLesson()
val currentLessonIdx: Int? = currentLessonEntry?.key
val currentLesson: Lesson? = currentLessonEntry?.value
val nextLessonEntry = day!!.getDistanceToNextByIdx(currentLessonIdx)
val nextLesson =
if (nextLessonEntry == null)
null
else
day!!.lessons[nextLessonEntry.key]
if (currentLesson == null && nextLesson == null) {
val notification = NotificationCompat
.Builder(applicationContext, NotificationChannels.LESSON_VIEW)
.setSmallIcon(R.drawable.logo)
.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()
return
}
val firstLessonIdx = day!!.getDistanceToNextByMinutes(0)?.key
?: throw NullPointerException("Is this even real?")
val distanceToFirst = day!!.lessons[firstLessonIdx]!!.time!!.start - currentMinutes
val currentLessonDelay =
if (currentLesson == null) // Если эта пара - перемена, то конец перемены через (результат getDistanceToNext)
nextLessonEntry!!.value
else // Если эта пара - обычная пара, то конец пары через (конец этой пары - текущее кол-во минут)
currentLesson.time!!.end - currentMinutes
val currentLessonName =
currentLesson?.getNameAndCabinetsShort(this@CurrentLessonViewService)
?: run {
if (distanceToFirst > 0)
getString(R.string.lessons_not_started)
else
getString(R.string.lesson_break)
}
val nextLessonName =
if (currentLesson == null) // Если текущая пара - перемена
nextLesson!!.getNameAndCabinetsShort(this@CurrentLessonViewService)
else if (nextLesson == null) // Если текущая пара - последняя
getString(R.string.lessons_end)
else // Если после текущей пары есть ещё пара(ы)
getString(R.string.lesson_break)
val nextLessonTotal =
if (currentLesson == null)
nextLesson!!.time!!.start
else
currentLesson.time!!.end
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
),
getString(
R.string.lesson_going_notification_description,
currentLessonName,
nextLessonTotal.fmtAsClock(),
nextLessonName,
)
)
getNotificationManager().notify(NOTIFICATION_STATUS_ID, notification)
handler.postDelayed(this, UPDATE_INTERVAL)
}
}
private fun createNotification(
title: String? = null,
description: String? = null
): Notification {
return NotificationCompat
.Builder(applicationContext, NotificationChannels.LESSON_VIEW)
.setSmallIcon(R.drawable.logo)
.setContentTitle(title ?: getString(R.string.lesson_notification_title))
.setContentText(description ?: getString(R.string.lesson_notification_description))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setOngoing(true)
.setSilent(true)
.build()
}
private fun getNotificationManager(): NotificationManager {
return getSystemService(NOTIFICATION_SERVICE) as NotificationManager
}
fun updateSchedule(group: Group?): Boolean {
if (group == null) {
stopSelf()
return false
}
val day = group.getCurrentDay()
if (day?.value == null) {
stopSelf()
return false
}
val dayValue = day.value!!
if (this.day == null) {
if (dayValue.lessons[dayValue.defaultIndices[dayValue.defaultIndices.lastIndex]]!!.time!!.end
<= Calendar.getInstance()
.get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance()
.get(Calendar.MINUTE)
) {
stopSelf()
return false
}
}
this.day = dayValue
this.handler.removeCallbacks(updateRunnable)
updateRunnable.run()
return true
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (!(applicationContext as PolytechnicApplication).hasNotificationPermission()) {
stopSelf()
return START_STICKY
}
if (intent == null)
throw NullPointerException("Intent shouldn't be null!")
val notification = createNotification()
startForeground(NOTIFICATION_STATUS_ID, notification)
if (!updateSchedule(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("group", Group::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra("group")
}
)
)
updateRunnable.run()
return START_STICKY
}
override fun onBind(p0: Intent?): IBinder? {
return null
}
}

View File

@@ -78,6 +78,7 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
} }
notify(id.hashCode(), notification) notify(id.hashCode(), notification)
CurrentLessonViewService.startService(applicationContext)
} }
} }

View File

@@ -3,8 +3,7 @@ package ru.n08i40k.polytechnic.next.ui
import android.Manifest import android.Manifest
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.pm.PackageManager import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
@@ -18,7 +17,6 @@ import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeContent import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
@@ -39,6 +37,8 @@ import kotlinx.coroutines.launch
import ru.n08i40k.polytechnic.next.NotificationChannels import ru.n08i40k.polytechnic.next.NotificationChannels
import ru.n08i40k.polytechnic.next.PolytechnicApplication import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.service.CurrentLessonViewService
import ru.n08i40k.polytechnic.next.settings.settingsDataStore import ru.n08i40k.polytechnic.next.settings.settingsDataStore
import ru.n08i40k.polytechnic.next.work.FcmUpdateCallbackWorker import ru.n08i40k.polytechnic.next.work.FcmUpdateCallbackWorker
import ru.n08i40k.polytechnic.next.work.LinkUpdateWorker import ru.n08i40k.polytechnic.next.work.LinkUpdateWorker
@@ -66,7 +66,7 @@ class MainActivity : ComponentActivity() {
} }
private fun createNotificationChannels() { private fun createNotificationChannels() {
if (!hasNotificationPermission()) if (!(applicationContext as PolytechnicApplication).hasNotificationPermission())
return return
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
@@ -84,6 +84,13 @@ class MainActivity : ComponentActivity() {
getString(R.string.app_update_channel_description), getString(R.string.app_update_channel_description),
NotificationChannels.APP_UPDATE NotificationChannels.APP_UPDATE
) )
createNotificationChannel(
notificationManager,
getString(R.string.lesson_view_channel_name),
getString(R.string.lesson_view_channel_description),
NotificationChannels.LESSON_VIEW
)
} }
private val requestPermissionLauncher = private val requestPermissionLauncher =
@@ -91,15 +98,30 @@ class MainActivity : ComponentActivity() {
if (it) createNotificationChannels() if (it) createNotificationChannels()
} }
private fun hasNotificationPermission(): Boolean { private fun askNotificationPermission() {
return (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU if (!(applicationContext as PolytechnicApplication).hasNotificationPermission())
|| ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED)
} }
private fun askNotificationPermission() { private fun startTestService() {
if (!hasNotificationPermission()) if (!(applicationContext as PolytechnicApplication).hasNotificationPermission())
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) return
lifecycleScope.launch {
val schedule = (applicationContext as PolytechnicApplication)
.container
.scheduleRepository
.getGroup()
if (schedule is MyResult.Failure)
return@launch
val intent = Intent(this@MainActivity, CurrentLessonViewService::class.java)
.apply {
putExtra("group", (schedule as MyResult.Success).data)
}
startForegroundService(intent)
}
} }
@@ -168,6 +190,7 @@ class MainActivity : ComponentActivity() {
setupFirebaseConfig() setupFirebaseConfig()
handleUpdate() handleUpdate()
startTestService()
setContent { setContent {
Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) { Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) {

View File

@@ -67,8 +67,8 @@ fun calculateCurrentLessonIdx(lessons: ArrayList<Lesson?>): Int {
.filterNotNull() .filterNotNull()
.filter { .filter {
it.time != null it.time != null
&& it.time.start >= currentMinutes && it.time.start <= currentMinutes
&& it.time.end <= currentMinutes && it.time.end >= currentMinutes
} }
if (filteredLessons.isEmpty()) if (filteredLessons.isEmpty())

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardColors import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
@@ -30,26 +31,20 @@ import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
import ru.n08i40k.polytechnic.next.model.Day import ru.n08i40k.polytechnic.next.model.Day
import ru.n08i40k.polytechnic.next.model.Lesson import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.model.LessonTime import ru.n08i40k.polytechnic.next.model.LessonTime
import ru.n08i40k.polytechnic.next.utils.fmtAsClock
private enum class LessonTimeFormat { private enum class LessonTimeFormat {
FROM_TO, ONLY_MINUTES_DURATION FROM_TO, ONLY_MINUTES_DURATION
} }
private fun numWithZero(num: Int): String {
return "0".repeat(if (num <= 9) 1 else 0) + num.toString()
}
@Composable @Composable
private fun fmtTime(start: Int, end: Int, format: LessonTimeFormat): ArrayList<String> { private fun fmtTime(start: Int, end: Int, format: LessonTimeFormat): ArrayList<String> {
return when (format) { return when (format) {
LessonTimeFormat.FROM_TO -> { LessonTimeFormat.FROM_TO -> {
val startHour = numWithZero(start / 60) val startClock = start.fmtAsClock()
val startMinute = numWithZero(start % 60) val endClock = end.fmtAsClock()
val endHour = numWithZero(end / 60) arrayListOf(startClock, endClock)
val endMinute = numWithZero(end % 60)
arrayListOf("$startHour:$startMinute", "$endHour:$endMinute")
} }
LessonTimeFormat.ONLY_MINUTES_DURATION -> { LessonTimeFormat.ONLY_MINUTES_DURATION -> {
@@ -86,13 +81,10 @@ fun LessonExtraInfo(
val duration = val duration =
if (lesson.time != null) lesson.time.end - lesson.time.start else 0 if (lesson.time != null) lesson.time.end - lesson.time.start else 0
val hours = duration / 60 append(duration / 60)
val minutes = duration % 60
append(hours)
append(stringResource(R.string.hours)) append(stringResource(R.string.hours))
append(" ") append(" ")
append(minutes) append(duration % 60)
append(stringResource(R.string.minutes)) append(stringResource(R.string.minutes))
} }
Text(duration) Text(duration)
@@ -117,7 +109,10 @@ private fun LessonViewRow(
time: LessonTime? = LessonTime(0, 60), time: LessonTime? = LessonTime(0, 60),
timeFormat: LessonTimeFormat = LessonTimeFormat.FROM_TO, timeFormat: LessonTimeFormat = LessonTimeFormat.FROM_TO,
name: String = "Test", name: String = "Test",
teacherNames: ArrayList<String> = arrayListOf("Хомченко Н.Е.", "Хомченко Н.Е."), teacherNames: ArrayList<String> = arrayListOf(
"Хомченко Н.Е. (1 подggggggggggggggggggggggggggggggggggggggгруппа)",
"Хомченко Н.Е. (2 подгруппа)"
),
cabinets: ArrayList<String> = arrayListOf("14", "31"), cabinets: ArrayList<String> = arrayListOf("14", "31"),
cardColors: CardColors = CardDefaults.cardColors(), cardColors: CardColors = CardDefaults.cardColors(),
verticalPadding: Dp = 10.dp verticalPadding: Dp = 10.dp
@@ -125,6 +120,8 @@ private fun LessonViewRow(
val contentColor = val contentColor =
if (timeFormat == LessonTimeFormat.FROM_TO) cardColors.contentColor else cardColors.disabledContentColor if (timeFormat == LessonTimeFormat.FROM_TO) cardColors.contentColor else cardColors.disabledContentColor
val teacherNamesRepl = teacherNames.map { it.replace("подгруппа", "подгр.") }
Row( Row(
modifier = Modifier.padding(10.dp, verticalPadding), modifier = Modifier.padding(10.dp, verticalPadding),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -164,15 +161,12 @@ private fun LessonViewRow(
Column( Column(
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
val fraction = if (cabinets.size == 0) 1F Row(
else if (cabinets.any { it.contains("/") }) 0.9F modifier = Modifier.fillMaxWidth(),
else 0.925F horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f)) {
Row {
Column {
Text( Text(
modifier = Modifier.fillMaxWidth(fraction),
text = name, text = name,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
maxLines = 1, maxLines = 1,
@@ -180,10 +174,9 @@ private fun LessonViewRow(
color = contentColor color = contentColor
) )
for (listIdx: Int in 0..<teacherNames.size) { for (teacherName in teacherNamesRepl) {
Text( Text(
modifier = Modifier.fillMaxWidth(fraction), text = teacherName,
text = teacherNames[listIdx],
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = contentColor color = contentColor
@@ -191,18 +184,15 @@ private fun LessonViewRow(
} }
} }
Column { Column(modifier = Modifier.wrapContentWidth()) {
if (cabinets.size <= teacherNames.size) { if (cabinets.size <= teacherNamesRepl.size) {
Text( Text(
modifier = Modifier.fillMaxWidth(),
text = "", text = "",
maxLines = 1 maxLines = 1
) )
} }
for (listIdx: Int in 0..<cabinets.size) { for (listIdx: Int in 0..<cabinets.size) {
Text( Text(
modifier = Modifier.fillMaxWidth(),
text = cabinets[listIdx], text = cabinets[listIdx],
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,

View File

@@ -5,3 +5,24 @@ infix fun <T> T?.or(data: T): T {
return data return data
return this return this
} }
fun Int.fmtAsClockEntry(): String {
return "0".repeat(if (this <= 9) 1 else 0) + this.toString()
}
fun Int.fmtAsClock(): String {
val hours = this / 60
val minutes = this % 60
return hours.fmtAsClockEntry() + ":" + minutes.fmtAsClockEntry()
}
infix fun String.limit(count: Int): String {
if (this.length <= count)
return this
return this
.substring(0, count - 1)
.trimEnd()
.plus("")
}

View File

@@ -5,6 +5,7 @@ import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.PolytechnicApplication import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.service.CurrentLessonViewService
class LinkUpdateWorker(context: Context, params: WorkerParameters) : class LinkUpdateWorker(context: Context, params: WorkerParameters) :
Worker(context, params) { Worker(context, params) {
@@ -15,6 +16,9 @@ class LinkUpdateWorker(context: Context, params: WorkerParameters) :
.scheduleRepository .scheduleRepository
.getGroup() .getGroup()
} }
CurrentLessonViewService.startService(applicationContext)
return Result.success() return Result.success()
} }
} }

View File

@@ -0,0 +1,41 @@
package ru.n08i40k.polytechnic.next.work
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat.startForegroundService
import androidx.work.Worker
import androidx.work.WorkerParameters
import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.service.CurrentLessonViewService
class StartClvService(context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {
override fun doWork(): Result {
val schedule = runBlocking {
(applicationContext as PolytechnicApplication)
.container
.scheduleRepository
.getGroup()
}
if (schedule is MyResult.Failure)
return Result.success()
val intent = Intent(applicationContext, CurrentLessonViewService::class.java)
.apply {
putExtra("group", (schedule as MyResult.Success).data)
}
applicationContext.stopService(
Intent(
applicationContext,
CurrentLessonViewService::class.java
)
)
startForegroundService(applicationContext, intent)
return Result.success()
}
}

View File

@@ -51,4 +51,18 @@
<string name="app_update_channel_description">Информирует о выходе новой версии этого приложения</string> <string name="app_update_channel_description">Информирует о выходе новой версии этого приложения</string>
<string name="app_update_title">Вышла версия %1$s!</string> <string name="app_update_title">Вышла версия %1$s!</string>
<string name="app_update_description">Нажмите что бы загрузить обновление.</string> <string name="app_update_description">Нажмите что бы загрузить обновление.</string>
<string name="lesson_view_channel_name">Текущая пара</string>
<string name="lesson_view_channel_description">Отображает текущую пару или перемену в уведомлении</string>
<string name="lesson_notification_title">Загрузка расписания…</string>
<string name="lesson_notification_description">Это уведомление обновится в течении нескольких секунд!</string>
<string name="lesson_going_notification_title">До конца %1$d ч. %2$d мин.</string>
<string name="lesson_going_notification_description">%1$s\n| Далее в %2$s - %3$s</string>
<string name="lessons_end">Конец пар</string>
<string name="lessons_end_notification_title">Пары закончились!</string>
<string name="lessons_end_notification_description">Ура, можно идти домой! Наверное :(</string>
<string name="cabinets_short_lc">каб.</string>
<string name="in_cabinets_short_lc">в %1$s каб.</string>
<string name="in_gym_lc">в спорт-зале</string>
<string name="lessons_not_started">Пары ещё не начались</string>
<string name="waiting_for_day_start_notification_title">До начала пар %1$d ч. %2$d мин.</string>
</resources> </resources>

View File

@@ -51,4 +51,18 @@
<string name="app_update_channel_description">Inform about new version of this app has been released</string> <string name="app_update_channel_description">Inform about new version of this app has been released</string>
<string name="app_update_title">Version %1$s released!</string> <string name="app_update_title">Version %1$s released!</string>
<string name="app_update_description">Click to download new version.</string> <string name="app_update_description">Click to download new version.</string>
<string name="lesson_view_channel_name">Current lesson</string>
<string name="lesson_view_channel_description">View current lesson and breaks in notification</string>
<string name="lesson_notification_title">Loading schedule…</string>
<string name="lesson_notification_description">This notification will be updated in several seconds!</string>
<string name="lesson_going_notification_title">To end %1$d h. %2$d min.</string>
<string name="lesson_going_notification_description">%1$s\n| After in %2$s - %3$s</string>
<string name="lessons_end">Lessons end</string>
<string name="lessons_end_notification_title">Lessons finished!</string>
<string name="lessons_end_notification_description">TODO</string>
<string name="cabinets_short_lc">cab.</string>
<string name="in_cabinets_short_lc">in %1$s cab.</string>
<string name="in_gym_lc">in gym</string>
<string name="lessons_not_started">Lessons haven\'t started yet</string>
<string name="waiting_for_day_start_notification_title">%1$d h. %2$d min. before lessons start</string>
</resources> </resources>