mirror of
https://github.com/n08i40k/polytechnic-android.git
synced 2025-12-06 09:47:48 +03:00
1.7.0
Отображение текущей пары в уведомлении. Мне на данный момент невероятно сложно написать код для запуска сервиса перед началом пар (мне лень), поэтому сервис будет запускаться каждые 7 утра с задежкой до 15 минут.
This commit is contained in:
4
.idea/inspectionProfiles/Project_Default.xml
generated
4
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -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
13
.idea/runConfigurations.xml
generated
Normal 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>
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(", ")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,7 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
notify(id.hashCode(), notification)
|
notify(id.hashCode(), notification)
|
||||||
|
CurrentLessonViewService.startService(applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))) {
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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("…")
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user