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="previewFile" value="true" />
|
||||
</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">
|
||||
<option name="composableFile" 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)
|
||||
|
||||
kotlin("plugin.serialization") version "2.0.20"
|
||||
id("kotlin-parcelize")
|
||||
|
||||
id("com.google.devtools.ksp")
|
||||
|
||||
@@ -32,8 +33,8 @@ android {
|
||||
applicationId = "ru.n08i40k.polytechnic.next"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 11
|
||||
versionName = "1.6.0"
|
||||
versionCode = 12
|
||||
versionName = "1.7.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<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
|
||||
android:name=".PolytechnicApplication"
|
||||
@@ -24,6 +26,15 @@
|
||||
</intent-filter>
|
||||
</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
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ru.n08i40k.polytechnic.next
|
||||
|
||||
object NotificationChannels {
|
||||
const val LESSON_VIEW = "lesson-view"
|
||||
const val SCHEDULE_UPDATE = "schedule-update"
|
||||
const val APP_UPDATE = "app-update"
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package ru.n08i40k.polytechnic.next
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import ru.n08i40k.polytechnic.next.data.AppContainer
|
||||
import ru.n08i40k.polytechnic.next.utils.or
|
||||
@@ -17,4 +21,10 @@ class PolytechnicApplication : Application() {
|
||||
.getPackageInfo(this.packageName, 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
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Calendar
|
||||
|
||||
@Parcelize
|
||||
@Suppress("unused")
|
||||
@Serializable
|
||||
class Day(
|
||||
@@ -10,4 +14,62 @@ class Day(
|
||||
val defaultIndices: ArrayList<Int>,
|
||||
val customIndices: ArrayList<Int>,
|
||||
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
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Calendar
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
class Group(
|
||||
data class Group(
|
||||
val name: String,
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.utils.limit
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data class Lesson(
|
||||
val type: LessonType,
|
||||
@@ -10,4 +16,37 @@ data class Lesson(
|
||||
val time: LessonTime?,
|
||||
val cabinets: 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
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Parcelize
|
||||
@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)
|
||||
CurrentLessonViewService.startService(applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ package ru.n08i40k.polytechnic.next.ui
|
||||
import android.Manifest
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
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.windowInsetsPadding
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.work.BackoffPolicy
|
||||
@@ -39,6 +37,8 @@ import kotlinx.coroutines.launch
|
||||
import ru.n08i40k.polytechnic.next.NotificationChannels
|
||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||
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.work.FcmUpdateCallbackWorker
|
||||
import ru.n08i40k.polytechnic.next.work.LinkUpdateWorker
|
||||
@@ -66,7 +66,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
private fun createNotificationChannels() {
|
||||
if (!hasNotificationPermission())
|
||||
if (!(applicationContext as PolytechnicApplication).hasNotificationPermission())
|
||||
return
|
||||
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
@@ -84,6 +84,13 @@ class MainActivity : ComponentActivity() {
|
||||
getString(R.string.app_update_channel_description),
|
||||
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 =
|
||||
@@ -91,15 +98,30 @@ class MainActivity : ComponentActivity() {
|
||||
if (it) createNotificationChannels()
|
||||
}
|
||||
|
||||
private fun hasNotificationPermission(): Boolean {
|
||||
return (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
|| ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
== PackageManager.PERMISSION_GRANTED)
|
||||
private fun askNotificationPermission() {
|
||||
if (!(applicationContext as PolytechnicApplication).hasNotificationPermission())
|
||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
|
||||
private fun askNotificationPermission() {
|
||||
if (!hasNotificationPermission())
|
||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
private fun startTestService() {
|
||||
if (!(applicationContext as PolytechnicApplication).hasNotificationPermission())
|
||||
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()
|
||||
|
||||
handleUpdate()
|
||||
startTestService()
|
||||
|
||||
setContent {
|
||||
Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) {
|
||||
|
||||
@@ -67,8 +67,8 @@ fun calculateCurrentLessonIdx(lessons: ArrayList<Lesson?>): Int {
|
||||
.filterNotNull()
|
||||
.filter {
|
||||
it.time != null
|
||||
&& it.time.start >= currentMinutes
|
||||
&& it.time.end <= currentMinutes
|
||||
&& it.time.start <= currentMinutes
|
||||
&& it.time.end >= currentMinutes
|
||||
}
|
||||
|
||||
if (filteredLessons.isEmpty())
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardColors
|
||||
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.Lesson
|
||||
import ru.n08i40k.polytechnic.next.model.LessonTime
|
||||
import ru.n08i40k.polytechnic.next.utils.fmtAsClock
|
||||
|
||||
private enum class LessonTimeFormat {
|
||||
FROM_TO, ONLY_MINUTES_DURATION
|
||||
}
|
||||
|
||||
private fun numWithZero(num: Int): String {
|
||||
return "0".repeat(if (num <= 9) 1 else 0) + num.toString()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun fmtTime(start: Int, end: Int, format: LessonTimeFormat): ArrayList<String> {
|
||||
return when (format) {
|
||||
LessonTimeFormat.FROM_TO -> {
|
||||
val startHour = numWithZero(start / 60)
|
||||
val startMinute = numWithZero(start % 60)
|
||||
val startClock = start.fmtAsClock()
|
||||
val endClock = end.fmtAsClock()
|
||||
|
||||
val endHour = numWithZero(end / 60)
|
||||
val endMinute = numWithZero(end % 60)
|
||||
|
||||
arrayListOf("$startHour:$startMinute", "$endHour:$endMinute")
|
||||
arrayListOf(startClock, endClock)
|
||||
}
|
||||
|
||||
LessonTimeFormat.ONLY_MINUTES_DURATION -> {
|
||||
@@ -86,13 +81,10 @@ fun LessonExtraInfo(
|
||||
val duration =
|
||||
if (lesson.time != null) lesson.time.end - lesson.time.start else 0
|
||||
|
||||
val hours = duration / 60
|
||||
val minutes = duration % 60
|
||||
|
||||
append(hours)
|
||||
append(duration / 60)
|
||||
append(stringResource(R.string.hours))
|
||||
append(" ")
|
||||
append(minutes)
|
||||
append(duration % 60)
|
||||
append(stringResource(R.string.minutes))
|
||||
}
|
||||
Text(duration)
|
||||
@@ -117,7 +109,10 @@ private fun LessonViewRow(
|
||||
time: LessonTime? = LessonTime(0, 60),
|
||||
timeFormat: LessonTimeFormat = LessonTimeFormat.FROM_TO,
|
||||
name: String = "Test",
|
||||
teacherNames: ArrayList<String> = arrayListOf("Хомченко Н.Е.", "Хомченко Н.Е."),
|
||||
teacherNames: ArrayList<String> = arrayListOf(
|
||||
"Хомченко Н.Е. (1 подggggggggggggggggggggggggggggggggggggggгруппа)",
|
||||
"Хомченко Н.Е. (2 подгруппа)"
|
||||
),
|
||||
cabinets: ArrayList<String> = arrayListOf("14", "31"),
|
||||
cardColors: CardColors = CardDefaults.cardColors(),
|
||||
verticalPadding: Dp = 10.dp
|
||||
@@ -125,6 +120,8 @@ private fun LessonViewRow(
|
||||
val contentColor =
|
||||
if (timeFormat == LessonTimeFormat.FROM_TO) cardColors.contentColor else cardColors.disabledContentColor
|
||||
|
||||
val teacherNamesRepl = teacherNames.map { it.replace("подгруппа", "подгр.") }
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(10.dp, verticalPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -164,15 +161,12 @@ private fun LessonViewRow(
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
val fraction = if (cabinets.size == 0) 1F
|
||||
else if (cabinets.any { it.contains("/") }) 0.9F
|
||||
else 0.925F
|
||||
|
||||
|
||||
Row {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(fraction),
|
||||
text = name,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
@@ -180,10 +174,9 @@ private fun LessonViewRow(
|
||||
color = contentColor
|
||||
)
|
||||
|
||||
for (listIdx: Int in 0..<teacherNames.size) {
|
||||
for (teacherName in teacherNamesRepl) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(fraction),
|
||||
text = teacherNames[listIdx],
|
||||
text = teacherName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = contentColor
|
||||
@@ -191,18 +184,15 @@ private fun LessonViewRow(
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
if (cabinets.size <= teacherNames.size) {
|
||||
Column(modifier = Modifier.wrapContentWidth()) {
|
||||
if (cabinets.size <= teacherNamesRepl.size) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "",
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
for (listIdx: Int in 0..<cabinets.size) {
|
||||
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = cabinets[listIdx],
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
||||
@@ -4,4 +4,25 @@ infix fun <T> T?.or(data: T): T {
|
||||
if (this == null)
|
||||
return data
|
||||
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 kotlinx.coroutines.runBlocking
|
||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||
import ru.n08i40k.polytechnic.next.service.CurrentLessonViewService
|
||||
|
||||
class LinkUpdateWorker(context: Context, params: WorkerParameters) :
|
||||
Worker(context, params) {
|
||||
@@ -15,6 +16,9 @@ class LinkUpdateWorker(context: Context, params: WorkerParameters) :
|
||||
.scheduleRepository
|
||||
.getGroup()
|
||||
}
|
||||
|
||||
CurrentLessonViewService.startService(applicationContext)
|
||||
|
||||
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_title">Вышла версия %1$s!</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>
|
||||
@@ -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_title">Version %1$s released!</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>
|
||||
Reference in New Issue
Block a user