mirror of
https://github.com/n08i40k/polytechnic-android.git
synced 2025-12-06 09:47:48 +03:00
3.1.0
Возвращено и исправлено отображение текущих пар в уведомлении. Исправлен баг при котором происходил выхода из аккаунта. Исправлен баг с неправильным отображением "сегодня", "завтра" и т.п.
This commit is contained in:
@@ -46,8 +46,8 @@ android {
|
|||||||
applicationId = "ru.n08i40k.polytechnic.next"
|
applicationId = "ru.n08i40k.polytechnic.next"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 25
|
versionCode = 27
|
||||||
versionName = "3.0.1"
|
versionName = "3.1.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -55,6 +55,8 @@ android {
|
|||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
|
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
|
|||||||
@@ -13,11 +13,11 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".Application"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:name=".Application"
|
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
@@ -32,6 +32,15 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".service.DayViewService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="specialUse">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="Service for viewing current lessons in notification." />
|
||||||
|
</service>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
@@ -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 com.google.android.gms.tasks.OnCompleteListener
|
import com.google.android.gms.tasks.OnCompleteListener
|
||||||
import com.google.android.gms.tasks.Task
|
import com.google.android.gms.tasks.Task
|
||||||
import com.google.firebase.messaging.FirebaseMessaging
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
@@ -36,40 +40,43 @@ class Application : Application() {
|
|||||||
get() = applicationContext.packageManager
|
get() = applicationContext.packageManager
|
||||||
.getPackageInfo(this.packageName, 0)
|
.getPackageInfo(this.packageName, 0)
|
||||||
.versionName!!
|
.versionName!!
|
||||||
// val version
|
|
||||||
// get() = "2.0.2"
|
|
||||||
|
|
||||||
private fun scheduleUpdateLinkWorker() {
|
// permissions
|
||||||
container.remoteConfig.activate().addOnCompleteListener {
|
val hasNotificationPermission: Boolean
|
||||||
UpdateLinkWorker.schedule(this@Application)
|
get() =
|
||||||
}
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||||
}
|
|| ContextCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
private fun fixupToken() {
|
private fun setupFirebase() {
|
||||||
if (runBlocking { settings.data.map { it.fcmToken }.first() }.isNotEmpty())
|
fun scheduleUpdateLinkWorker() {
|
||||||
return
|
container.remoteConfig.activate().addOnCompleteListener {
|
||||||
|
UpdateLinkWorker.schedule(this@Application)
|
||||||
FirebaseMessaging.getInstance().token.addOnCompleteListener(object :
|
|
||||||
OnCompleteListener<String> {
|
|
||||||
override fun onComplete(token: Task<String?>) {
|
|
||||||
if (!token.isSuccessful)
|
|
||||||
return
|
|
||||||
|
|
||||||
UpdateFCMTokenWorker.schedule(applicationContext, token.result!!)
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
fun fixupToken() {
|
||||||
super.onCreate()
|
if (runBlocking { settings.data.map { it.fcmToken }.first() }.isNotEmpty())
|
||||||
|
return
|
||||||
|
|
||||||
VKID.init(this)
|
FirebaseMessaging.getInstance().token.addOnCompleteListener(object :
|
||||||
|
OnCompleteListener<String> {
|
||||||
|
override fun onComplete(token: Task<String?>) {
|
||||||
|
if (!token.isSuccessful)
|
||||||
|
return
|
||||||
|
|
||||||
|
UpdateFCMTokenWorker.schedule(applicationContext, token.result!!)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
val remoteConfig = container.remoteConfig
|
val remoteConfig = container.remoteConfig
|
||||||
|
|
||||||
remoteConfig.setConfigSettingsAsync(remoteConfigSettings {
|
remoteConfig.setConfigSettingsAsync(
|
||||||
minimumFetchIntervalInSeconds = 3600
|
remoteConfigSettings { minimumFetchIntervalInSeconds = 3600 }
|
||||||
})
|
)
|
||||||
remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
|
remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
|
||||||
|
|
||||||
remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener {
|
remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener {
|
||||||
@@ -86,4 +93,12 @@ class Application : Application() {
|
|||||||
scheduleUpdateLinkWorker()
|
scheduleUpdateLinkWorker()
|
||||||
fixupToken()
|
fixupToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
VKID.init(this)
|
||||||
|
|
||||||
|
setupFirebase()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,6 @@ package ru.n08i40k.polytechnic.next
|
|||||||
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.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@@ -19,7 +17,6 @@ import androidx.compose.foundation.layout.safeContent
|
|||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
@@ -28,6 +25,7 @@ import ru.n08i40k.polytechnic.next.app.NotificationChannels
|
|||||||
import ru.n08i40k.polytechnic.next.settings.settings
|
import ru.n08i40k.polytechnic.next.settings.settings
|
||||||
import ru.n08i40k.polytechnic.next.ui.PolytechnicApp
|
import ru.n08i40k.polytechnic.next.ui.PolytechnicApp
|
||||||
import ru.n08i40k.polytechnic.next.ui.theme.AppTheme
|
import ru.n08i40k.polytechnic.next.ui.theme.AppTheme
|
||||||
|
import ru.n08i40k.polytechnic.next.utils.app
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -44,9 +42,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createNotificationChannels() {
|
private fun createNotificationChannels() {
|
||||||
if (!hasNotificationPermission())
|
|
||||||
return
|
|
||||||
|
|
||||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
createNotificationChannel(
|
createNotificationChannel(
|
||||||
@@ -63,37 +58,33 @@ class MainActivity : ComponentActivity() {
|
|||||||
NotificationChannels.APP_UPDATE
|
NotificationChannels.APP_UPDATE
|
||||||
)
|
)
|
||||||
|
|
||||||
// createNotificationChannel(
|
createNotificationChannel(
|
||||||
// notificationManager,
|
notificationManager,
|
||||||
// getString(R.string.lesson_view_channel_name),
|
getString(R.string.day_view_channel_name),
|
||||||
// getString(R.string.lesson_view_channel_description),
|
getString(R.string.day_view_channel_description),
|
||||||
// NotificationChannels.LESSON_VIEW
|
NotificationChannels.DAY_VIEW
|
||||||
// )
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val requestPermissionLauncher =
|
private val notificationRPL =
|
||||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||||
if (it) createNotificationChannels()
|
if (it) createNotificationChannels()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun askNotificationPermission() {
|
private fun setupNotifications() {
|
||||||
if (hasNotificationPermission())
|
if (app.hasNotificationPermission) {
|
||||||
|
createNotificationChannels()
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
notificationRPL.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasNotificationPermission(): Boolean =
|
|
||||||
(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
|
||||||
|| ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
== PackageManager.PERMISSION_GRANTED)
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
askNotificationPermission()
|
setupNotifications()
|
||||||
createNotificationChannels()
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
settings.data.first()
|
settings.data.first()
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ package ru.n08i40k.polytechnic.next.app
|
|||||||
object NotificationChannels {
|
object NotificationChannels {
|
||||||
const val SCHEDULE_UPDATE = "schedule-update"
|
const val SCHEDULE_UPDATE = "schedule-update"
|
||||||
const val APP_UPDATE = "app-update"
|
const val APP_UPDATE = "app-update"
|
||||||
|
const val DAY_VIEW = "day-view"
|
||||||
}
|
}
|
||||||
@@ -31,12 +31,4 @@ data class GroupOrTeacher(
|
|||||||
get() {
|
get() {
|
||||||
return days.getOrNull(currentIdx ?: return null)
|
return days.getOrNull(currentIdx ?: return null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: вернуть
|
|
||||||
@Suppress("unused")
|
|
||||||
val currentKV: Pair<Int, Day>?
|
|
||||||
get() {
|
|
||||||
val idx = currentIdx ?: return null
|
|
||||||
return Pair(idx, days[idx])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@ import android.os.Parcelable
|
|||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import ru.n08i40k.polytechnic.next.R
|
import ru.n08i40k.polytechnic.next.R
|
||||||
import ru.n08i40k.polytechnic.next.utils.dayMinutes
|
|
||||||
import ru.n08i40k.polytechnic.next.utils.limit
|
import ru.n08i40k.polytechnic.next.utils.limit
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -18,25 +17,22 @@ data class Lesson(
|
|||||||
val group: String? = null,
|
val group: String? = null,
|
||||||
val subGroups: List<SubGroup>
|
val subGroups: List<SubGroup>
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
// TODO: вернуть
|
fun getShortName(context: Context): String {
|
||||||
@Suppress("unused")
|
|
||||||
val duration get() = time.end.dayMinutes - time.start.dayMinutes
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
fun getNameAndCabinetsShort(context: Context): String {
|
|
||||||
val name =
|
val name =
|
||||||
if (type == LessonType.BREAK) context.getString(
|
if (type == LessonType.BREAK)
|
||||||
if (group == null)
|
context.getString(
|
||||||
R.string.student_break
|
if (group == null)
|
||||||
else
|
R.string.student_break
|
||||||
R.string.teacher_break
|
else
|
||||||
)
|
R.string.teacher_break
|
||||||
else this.name
|
)
|
||||||
|
else
|
||||||
|
this.name
|
||||||
|
|
||||||
val shortName = name!! limit 15
|
val shortName = name!! limit 15
|
||||||
val cabinetList = subGroups.map { it.cabinet }
|
val cabinetList = subGroups.map { it.cabinet }
|
||||||
|
|
||||||
if (cabinetList.isEmpty())
|
if (cabinetList.isEmpty() || (cabinetList.size == 1 && cabinetList[0].isEmpty()))
|
||||||
return shortName
|
return shortName
|
||||||
|
|
||||||
if (cabinetList.size == 1 && cabinetList[0] == "с/з")
|
if (cabinetList.size == 1 && cabinetList[0] == "с/з")
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package ru.n08i40k.polytechnic.next.network.request
|
|||||||
import com.android.volley.AuthFailureError
|
import com.android.volley.AuthFailureError
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import com.android.volley.VolleyError
|
import com.android.volley.VolleyError
|
||||||
import jakarta.inject.Singleton
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@@ -22,30 +21,12 @@ open class AuthorizedRequest(
|
|||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
listener,
|
listener,
|
||||||
@Singleton
|
|
||||||
object : Response.ErrorListener {
|
object : Response.ErrorListener {
|
||||||
override fun onErrorResponse(error: VolleyError?) {
|
override fun onErrorResponse(error: VolleyError?) {
|
||||||
val context = appContainer.context
|
if (!canBeUnauthorized && error is AuthFailureError)
|
||||||
|
runBlocking { appContainer.profileRepository.signOut() }
|
||||||
if (!canBeUnauthorized && error is AuthFailureError) {
|
|
||||||
runBlocking {
|
|
||||||
context.settings.updateData { currentSettings ->
|
|
||||||
currentSettings
|
|
||||||
.toBuilder()
|
|
||||||
.clear()
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: если не авторизован
|
|
||||||
// if (context.profileViewModel != null)
|
|
||||||
// context.profileViewModel!!.onUnauthorized()
|
|
||||||
}
|
|
||||||
|
|
||||||
runBlocking { appContainer.profileRepository.signOut() }
|
|
||||||
|
|
||||||
errorListener?.onErrorResponse(error)
|
errorListener?.onErrorResponse(error)
|
||||||
|
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
@@ -58,15 +39,11 @@ open class AuthorizedRequest(
|
|||||||
.first()
|
.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: если не авторизован
|
|
||||||
// if (accessToken.isEmpty() && context.profileViewModel != null)
|
|
||||||
// context.profileViewModel!!.onUnauthorized()
|
|
||||||
|
|
||||||
val headers = super.getHeaders()
|
val headers = super.getHeaders()
|
||||||
headers["Authorization"] = "Bearer $accessToken"
|
headers["Authorization"] = "Bearer $accessToken"
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
val appContext get() = appContainer.context
|
protected val appContext get() = appContainer.context
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,6 @@ open class CachedRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun send(context: Context) {
|
override fun send(context: Context) {
|
||||||
// TODO: network cache
|
|
||||||
val logger = Logger.getLogger("CachedRequest")
|
val logger = Logger.getLogger("CachedRequest")
|
||||||
val cache = appContainer.networkCacheRepository
|
val cache = appContainer.networkCacheRepository
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
package ru.n08i40k.polytechnic.next.service
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat.startForegroundService
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import ru.n08i40k.polytechnic.next.Application
|
||||||
|
import ru.n08i40k.polytechnic.next.R
|
||||||
|
import ru.n08i40k.polytechnic.next.app.NotificationChannels
|
||||||
|
import ru.n08i40k.polytechnic.next.app.appContainer
|
||||||
|
import ru.n08i40k.polytechnic.next.model.Day
|
||||||
|
import ru.n08i40k.polytechnic.next.model.UserRole
|
||||||
|
import ru.n08i40k.polytechnic.next.utils.MyResult
|
||||||
|
import ru.n08i40k.polytechnic.next.utils.dayMinutes
|
||||||
|
import ru.n08i40k.polytechnic.next.utils.fmtAsClock
|
||||||
|
import ru.n08i40k.polytechnic.next.utils.now
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
class DayViewService : Service() {
|
||||||
|
private val coroutineScope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = Logger.getLogger("DayView")
|
||||||
|
|
||||||
|
private const val NOTIFICATION_MAIN_ID = 3141_00
|
||||||
|
private const val NOTIFICATION_END_ID = 3141_59
|
||||||
|
|
||||||
|
private const val UPDATE_INTERVAL_MILLIS = 1_000L
|
||||||
|
|
||||||
|
fun start(app: Application) {
|
||||||
|
if (!app.hasNotificationPermission) {
|
||||||
|
logger.warning("Cannot start service, because app don't have notifications permission!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(app, DayViewService::class.java)
|
||||||
|
|
||||||
|
app.stopService(intent)
|
||||||
|
startForegroundService(app, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var day: Day
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
private fun onLessonsEnd() {
|
||||||
|
notificationManager.notify(
|
||||||
|
NOTIFICATION_END_ID,
|
||||||
|
NotificationCompat
|
||||||
|
.Builder(this, NotificationChannels.DAY_VIEW)
|
||||||
|
.setSmallIcon(R.drawable.schedule)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setContentTitle(getString(R.string.day_view_end_title))
|
||||||
|
.setContentText(getString(R.string.day_view_end_description))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
handler.removeCallbacks(runnable)
|
||||||
|
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val runnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
val (currentIndex, current) = day.currentKV ?: (null to null)
|
||||||
|
val (nextIndex, distanceToNext) = day.distanceToNext(currentIndex) ?: (null to null)
|
||||||
|
if (current == null && nextIndex == null) {
|
||||||
|
onLessonsEnd()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.postDelayed(this, UPDATE_INTERVAL_MILLIS)
|
||||||
|
|
||||||
|
val context = this@DayViewService
|
||||||
|
|
||||||
|
val next = nextIndex?.let { day.lessons[nextIndex] }
|
||||||
|
val nextName = next?.getShortName(context) ?: getString(R.string.day_view_lessons_end)
|
||||||
|
|
||||||
|
val nowMinutes = LocalDateTime.now().dayMinutes
|
||||||
|
val eventMinutes = (current?.time?.end ?: next!!.time.start).dayMinutes
|
||||||
|
|
||||||
|
// Если следующая пара - первая.
|
||||||
|
// Пока что вариантов, когда текущая пара null, а следующая нет я не видел.
|
||||||
|
if (current == null) {
|
||||||
|
notificationManager.notify(
|
||||||
|
NOTIFICATION_MAIN_ID,
|
||||||
|
createNotification(
|
||||||
|
getString(
|
||||||
|
R.string.day_view_wait_for_begin_title,
|
||||||
|
(eventMinutes - nowMinutes) / 60,
|
||||||
|
(eventMinutes - nowMinutes) % 60
|
||||||
|
),
|
||||||
|
getString(
|
||||||
|
R.string.day_view_going_description,
|
||||||
|
getString(R.string.day_view_not_started),
|
||||||
|
eventMinutes.fmtAsClock(),
|
||||||
|
nextName,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.notify(
|
||||||
|
NOTIFICATION_MAIN_ID,
|
||||||
|
createNotification(
|
||||||
|
getString(
|
||||||
|
R.string.day_view_going_title,
|
||||||
|
(eventMinutes - nowMinutes) / 60,
|
||||||
|
(eventMinutes - nowMinutes) % 60
|
||||||
|
),
|
||||||
|
getString(
|
||||||
|
R.string.day_view_going_description,
|
||||||
|
current.getShortName(context),
|
||||||
|
eventMinutes.fmtAsClock(),
|
||||||
|
nextName,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var notificationManager: NotificationManager
|
||||||
|
|
||||||
|
private fun createNotification(
|
||||||
|
title: String,
|
||||||
|
description: String
|
||||||
|
): Notification {
|
||||||
|
return NotificationCompat
|
||||||
|
.Builder(this, NotificationChannels.DAY_VIEW)
|
||||||
|
.setSmallIcon(R.drawable.schedule)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(description)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setSilent(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadSchedule(): Boolean {
|
||||||
|
val profileRepository = appContainer.profileRepository
|
||||||
|
val scheduleRepository = appContainer.scheduleRepository
|
||||||
|
|
||||||
|
val profile = when (val result = profileRepository.getProfile()) {
|
||||||
|
is MyResult.Success -> result.data
|
||||||
|
else -> {
|
||||||
|
logger.warning("Cannot start service, because get profile request failed!")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val schedule = when (
|
||||||
|
val result = when (profile.role) {
|
||||||
|
UserRole.TEACHER -> {
|
||||||
|
// TODO: implement schedule breaks for teachers on server-side.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> scheduleRepository.getGroup()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
is MyResult.Success -> result.data
|
||||||
|
else -> {
|
||||||
|
logger.warning("Cannot start service, because get schedule request failed!")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentDay = schedule.current
|
||||||
|
|
||||||
|
if (currentDay == null || currentDay.lessons.isEmpty()) {
|
||||||
|
logger.warning("Cannot start service, because no lessons today!")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Clock.System.now() > currentDay.lessons.first().time.start && currentDay.current == null) {
|
||||||
|
logger.warning("Cannot start service, because it started after lessons end!")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.day = currentDay
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
val notification = createNotification(
|
||||||
|
getString(R.string.day_view_title),
|
||||||
|
getString(R.string.day_view_description)
|
||||||
|
)
|
||||||
|
startForeground(NOTIFICATION_MAIN_ID, notification)
|
||||||
|
|
||||||
|
coroutineScope
|
||||||
|
.launch {
|
||||||
|
if (!loadSchedule()) {
|
||||||
|
stopSelf()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
this@DayViewService.handler.removeCallbacks(runnable)
|
||||||
|
this@DayViewService.runnable.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(p0: Intent?): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,19 +11,21 @@ import androidx.core.app.NotificationCompat
|
|||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
import com.google.firebase.messaging.RemoteMessage
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import ru.n08i40k.polytechnic.next.R
|
import ru.n08i40k.polytechnic.next.R
|
||||||
import ru.n08i40k.polytechnic.next.app.NotificationChannels
|
import ru.n08i40k.polytechnic.next.app.NotificationChannels
|
||||||
|
import ru.n08i40k.polytechnic.next.utils.app
|
||||||
import ru.n08i40k.polytechnic.next.worker.UpdateFCMTokenWorker
|
import ru.n08i40k.polytechnic.next.worker.UpdateFCMTokenWorker
|
||||||
|
|
||||||
|
|
||||||
|
private interface MessageHandler {
|
||||||
|
fun execute(service: FCMService)
|
||||||
|
}
|
||||||
|
|
||||||
private data class ScheduleUpdateData(
|
private data class ScheduleUpdateData(
|
||||||
val type: String,
|
val type: String,
|
||||||
val replaced: Boolean,
|
val replaced: Boolean,
|
||||||
val etag: String
|
val etag: String
|
||||||
) {
|
) : MessageHandler {
|
||||||
constructor(message: RemoteMessage) : this(
|
constructor(message: RemoteMessage) : this(
|
||||||
type = message.data["type"]
|
type = message.data["type"]
|
||||||
?: throw IllegalArgumentException("Type is missing in RemoteMessage"),
|
?: throw IllegalArgumentException("Type is missing in RemoteMessage"),
|
||||||
@@ -33,7 +35,7 @@ private data class ScheduleUpdateData(
|
|||||||
?: throw IllegalArgumentException("Etag is missing in RemoteMessage")
|
?: throw IllegalArgumentException("Etag is missing in RemoteMessage")
|
||||||
)
|
)
|
||||||
|
|
||||||
fun handleMessage(service: FCMService) {
|
override fun execute(service: FCMService) {
|
||||||
service.sendNotification(
|
service.sendNotification(
|
||||||
NotificationChannels.SCHEDULE_UPDATE,
|
NotificationChannels.SCHEDULE_UPDATE,
|
||||||
R.drawable.schedule,
|
R.drawable.schedule,
|
||||||
@@ -51,20 +53,14 @@ private data class ScheduleUpdateData(
|
|||||||
|
|
||||||
private data class LessonsStartData(
|
private data class LessonsStartData(
|
||||||
val type: String
|
val type: String
|
||||||
) {
|
) : MessageHandler {
|
||||||
constructor(message: RemoteMessage) : this(
|
constructor(message: RemoteMessage) : this(
|
||||||
type = message.data["type"]
|
type = message.data["type"]
|
||||||
?: throw IllegalArgumentException("Type is missing in RemoteMessage")
|
?: throw IllegalArgumentException("Type is missing in RemoteMessage")
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: вернуть
|
override fun execute(service: FCMService) {
|
||||||
@Suppress("unused")
|
DayViewService.start(service.app)
|
||||||
fun handleMessage(service: FCMService) {
|
|
||||||
// Uncomment and implement if needed
|
|
||||||
// service.scope.launch {
|
|
||||||
// CurrentLessonViewService
|
|
||||||
// .startService(service.applicationContext as PolytechnicApplication)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +68,7 @@ private data class AppUpdateData(
|
|||||||
val type: String,
|
val type: String,
|
||||||
val version: String,
|
val version: String,
|
||||||
val downloadLink: String
|
val downloadLink: String
|
||||||
) {
|
) : MessageHandler {
|
||||||
constructor(message: RemoteMessage) : this(
|
constructor(message: RemoteMessage) : this(
|
||||||
type = message.data["type"]
|
type = message.data["type"]
|
||||||
?: throw IllegalArgumentException("Type is missing in RemoteMessage"),
|
?: throw IllegalArgumentException("Type is missing in RemoteMessage"),
|
||||||
@@ -82,7 +78,7 @@ private data class AppUpdateData(
|
|||||||
?: throw IllegalArgumentException("DownloadLink is missing in RemoteMessage")
|
?: throw IllegalArgumentException("DownloadLink is missing in RemoteMessage")
|
||||||
)
|
)
|
||||||
|
|
||||||
fun handleMessage(service: FCMService) {
|
override fun execute(service: FCMService) {
|
||||||
service.sendNotification(
|
service.sendNotification(
|
||||||
NotificationChannels.APP_UPDATE,
|
NotificationChannels.APP_UPDATE,
|
||||||
R.drawable.download,
|
R.drawable.download,
|
||||||
@@ -95,10 +91,6 @@ private data class AppUpdateData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FCMService : FirebaseMessagingService() {
|
class FCMService : FirebaseMessagingService() {
|
||||||
// TODO: вернуть
|
|
||||||
@Suppress("unused")
|
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
|
||||||
|
|
||||||
override fun onNewToken(token: String) {
|
override fun onNewToken(token: String) {
|
||||||
super.onNewToken(token)
|
super.onNewToken(token)
|
||||||
|
|
||||||
@@ -149,10 +141,11 @@ class FCMService : FirebaseMessagingService() {
|
|||||||
val type = message.data["type"]
|
val type = message.data["type"]
|
||||||
|
|
||||||
when (type) {
|
when (type) {
|
||||||
"schedule-update" -> ScheduleUpdateData(message).handleMessage(this)
|
"schedule-update" -> ScheduleUpdateData(message)
|
||||||
"lessons-start" -> LessonsStartData(message).handleMessage(this)
|
"lessons-start" -> LessonsStartData(message)
|
||||||
"app-update" -> AppUpdateData(message).handleMessage(this)
|
"app-update" -> AppUpdateData(message)
|
||||||
}
|
else -> null
|
||||||
|
}?.execute(this)
|
||||||
|
|
||||||
super.onMessageReceived(message)
|
super.onMessageReceived(message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,8 +54,6 @@ class ProfileViewModel @Inject constructor(
|
|||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: сделать хук на unauthorized и сделать так что бы waiter удалялся, если сход контекст
|
|
||||||
|
|
||||||
fun refresh(): SingleHook<Profile?> {
|
fun refresh(): SingleHook<Profile?> {
|
||||||
val singleHook = SingleHook<Profile?>()
|
val singleHook = SingleHook<Profile?>()
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ fun DayCard(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
day: Day = MockScheduleRepository.exampleTeacher.days[0]
|
day: Day = MockScheduleRepository.exampleTeacher.days[0]
|
||||||
) {
|
) {
|
||||||
val offset = remember { getDayOffset(day) }
|
val offset = remember(day) { getDayOffset(day) }
|
||||||
|
|
||||||
val defaultCardColors = CardDefaults.cardColors(
|
val defaultCardColors = CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
|||||||
@@ -89,4 +89,15 @@
|
|||||||
<string name="schedule_channel_description">Уведомления об обновлении расписания</string>
|
<string name="schedule_channel_description">Уведомления об обновлении расписания</string>
|
||||||
<string name="app_update_channel_name">Обновления приложения</string>
|
<string name="app_update_channel_name">Обновления приложения</string>
|
||||||
<string name="app_update_channel_description">Уведомления о выходе новой версии этого приложения</string>
|
<string name="app_update_channel_description">Уведомления о выходе новой версии этого приложения</string>
|
||||||
|
<string name="day_view_title">Загрузка расписания…</string>
|
||||||
|
<string name="day_view_description">Это уведомление обновится в течение нескольких секунд!</string>
|
||||||
|
<string name="day_view_not_started">Пары ещё не начались</string>
|
||||||
|
<string name="day_view_going_title">До конца %1$d ч. %2$d мин.</string>
|
||||||
|
<string name="day_view_wait_for_begin_title">До начала пар %1$d ч. %2$d мин.</string>
|
||||||
|
<string name="day_view_lessons_end">Конец пар</string>
|
||||||
|
<string name="day_view_going_description">%1$s\n| Далее в %2$s - %3$s</string>
|
||||||
|
<string name="day_view_end_title">Пары закончились!</string>
|
||||||
|
<string name="day_view_end_description">Ура, можно идти домой! Наверное :(</string>
|
||||||
|
<string name="day_view_channel_name">Текущая пара</string>
|
||||||
|
<string name="day_view_channel_description">Отображает текущую пару или перемену в уведомлении</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -89,4 +89,15 @@
|
|||||||
<string name="schedule_channel_description">Inform when schedule has been updated</string>
|
<string name="schedule_channel_description">Inform when schedule has been updated</string>
|
||||||
<string name="app_update_channel_name">Application update</string>
|
<string name="app_update_channel_name">Application update</string>
|
||||||
<string name="app_update_channel_description">Inform about a new version of this app has been released</string>
|
<string name="app_update_channel_description">Inform about a new version of this app has been released</string>
|
||||||
|
<string name="day_view_title">Loading schedule…</string>
|
||||||
|
<string name="day_view_description">This notification will be updated in several seconds!</string>
|
||||||
|
<string name="day_view_wait_for_begin_title">%1$d h. %2$d min. before lessons start</string>
|
||||||
|
<string name="day_view_lessons_end">Lessons end</string>
|
||||||
|
<string name="day_view_going_description">%1$s\n| After in %2$s - %3$s</string>
|
||||||
|
<string name="day_view_not_started">Lessons haven\'t started yet</string>
|
||||||
|
<string name="day_view_going_title">To end %1$d h. %2$d min.</string>
|
||||||
|
<string name="day_view_end_title">Lessons finished!</string>
|
||||||
|
<string name="day_view_end_description">ya ne budu eto perevidit\'</string>
|
||||||
|
<string name="day_view_channel_name">Current lesson</string>
|
||||||
|
<string name="day_view_channel_description">View the current lesson and breaks in notification</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<defaults>
|
<defaults>
|
||||||
<entry>
|
<entry>
|
||||||
<key>serverVersion</key>
|
<key>serverVersion</key>
|
||||||
<value>2.2.2</value>
|
<value>3.0.1</value>
|
||||||
</entry>
|
</entry>
|
||||||
<entry>
|
<entry>
|
||||||
<key>linkUpdateDelay</key>
|
<key>linkUpdateDelay</key>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
</entry>
|
</entry>
|
||||||
<entry>
|
<entry>
|
||||||
<key>minVersion</key>
|
<key>minVersion</key>
|
||||||
<value>2.0.1</value>
|
<value>3.0.0</value>
|
||||||
</entry>
|
</entry>
|
||||||
<entry>
|
<entry>
|
||||||
<key>telegramLink</key>
|
<key>telegramLink</key>
|
||||||
@@ -22,10 +22,10 @@
|
|||||||
</entry>
|
</entry>
|
||||||
<entry>
|
<entry>
|
||||||
<key>downloadLink</key>
|
<key>downloadLink</key>
|
||||||
<value>https://t.me/polytechnic_next/68</value>
|
<value>https://t.me/polytechnic_next/99</value>
|
||||||
</entry>
|
</entry>
|
||||||
<entry>
|
<entry>
|
||||||
<key>currVersion</key>
|
<key>currVersion</key>
|
||||||
<value>2.2.1</value>
|
<value>3.1.0</value>
|
||||||
</entry>
|
</entry>
|
||||||
</defaults>
|
</defaults>
|
||||||
@@ -1,23 +1,4 @@
|
|||||||
# Project-wide Gradle settings.
|
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
|
||||||
# IDE (e.g. Android Studio) users:
|
|
||||||
# Gradle settings configured through the IDE *will override*
|
|
||||||
# any settings specified in this file.
|
|
||||||
# For more details on how to configure your build environment visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
|
||||||
# This option should only be used with decoupled projects. For more details, visit
|
|
||||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
|
||||||
# org.gradle.parallel=true
|
|
||||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
|
||||||
# Android operating system, and which are packaged with your app's APK
|
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
|
||||||
# thereby reducing the size of the R class for that library
|
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
@@ -8,17 +8,17 @@ junitVersion = "1.2.1"
|
|||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
lifecycleRuntimeKtx = "2.8.7"
|
lifecycleRuntimeKtx = "2.8.7"
|
||||||
activityCompose = "1.10.0"
|
activityCompose = "1.10.0"
|
||||||
composeBom = "2025.01.00"
|
composeBom = "2025.01.01"
|
||||||
accompanistSwiperefresh = "0.36.0"
|
accompanistSwiperefresh = "0.36.0"
|
||||||
firebaseBom = "33.8.0"
|
firebaseBom = "33.8.0"
|
||||||
hiltAndroid = "2.53.1"
|
hiltAndroid = "2.55"
|
||||||
hiltAndroidCompiler = "2.53.1"
|
hiltAndroidCompiler = "2.55"
|
||||||
hiltNavigationCompose = "1.2.0"
|
hiltNavigationCompose = "1.2.0"
|
||||||
kotlinxSerializationJson = "1.7.3"
|
kotlinxSerializationJson = "1.8.0"
|
||||||
protobufLite = "3.0.1"
|
protobufLite = "3.0.1"
|
||||||
volley = "1.2.1"
|
volley = "1.2.1"
|
||||||
datastore = "1.1.2"
|
datastore = "1.1.2"
|
||||||
navigationCompose = "2.8.5"
|
navigationCompose = "2.8.6"
|
||||||
googleFirebaseCrashlytics = "3.0.2"
|
googleFirebaseCrashlytics = "3.0.2"
|
||||||
workRuntime = "2.10.0"
|
workRuntime = "2.10.0"
|
||||||
vkid = "2.2.2"
|
vkid = "2.2.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user