diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml
index c6a9c7c..cb7184b 100644
--- a/.idea/appInsightsSettings.xml
+++ b/.idea/appInsightsSettings.xml
@@ -15,35 +15,8 @@
-
-
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index b589d56..b86273d 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 0897082..7b3006b 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,6 +4,7 @@
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 2904b84..5bb575c 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -7,7 +7,7 @@
-
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f8e50eb..7d01b75 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -32,8 +32,8 @@ android {
applicationId = "ru.n08i40k.polytechnic.next"
minSdk = 26
targetSdk = 35
- versionCode = 10
- versionName = "1.5.0"
+ versionCode = 11
+ versionName = "1.6.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -103,6 +103,7 @@ dependencies {
implementation(libs.firebase.analytics)
implementation(libs.firebase.messaging)
implementation(libs.firebase.crashlytics)
+ implementation(libs.firebase.config)
// datastore
implementation(libs.androidx.datastore)
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/NotificationChannels.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/NotificationChannels.kt
index f179277..03a8259 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/NotificationChannels.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/NotificationChannels.kt
@@ -2,4 +2,5 @@ package ru.n08i40k.polytechnic.next
object NotificationChannels {
const val SCHEDULE_UPDATE = "schedule-update"
+ const val APP_UPDATE = "app-update"
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/PolytechnicApplication.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/PolytechnicApplication.kt
index 5ce1553..98372e5 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/PolytechnicApplication.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/PolytechnicApplication.kt
@@ -3,6 +3,7 @@ package ru.n08i40k.polytechnic.next
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import ru.n08i40k.polytechnic.next.data.AppContainer
+import ru.n08i40k.polytechnic.next.utils.or
import javax.inject.Inject
@HiltAndroidApp
@@ -10,4 +11,10 @@ class PolytechnicApplication : Application() {
@Suppress("unused")
@Inject
lateinit var container: AppContainer
+
+ fun getAppVersion(): String {
+ return applicationContext.packageManager
+ .getPackageInfo(this.packageName, 0)
+ .versionName or "1.0.0"
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/AuthorizedRequest.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/AuthorizedRequest.kt
index fc4fe43..60ae574 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/AuthorizedRequest.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/AuthorizedRequest.kt
@@ -30,7 +30,8 @@ open class AuthorizedRequest(
.setAccessToken("").build()
}
}
- context.profileViewModel!!.onUnauthorized()
+ if (context.profileViewModel != null)
+ context.profileViewModel!!.onUnauthorized()
}
errorListener?.onErrorResponse(it)
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/CachedRequest.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/CachedRequest.kt
index 03694a6..dfde96c 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/CachedRequest.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/CachedRequest.kt
@@ -78,8 +78,6 @@ open class CachedRequest(
val logger = Logger.getLogger("CachedRequest")
val repository = appContainer.networkCacheRepository
- logger.info("Getting cache status...")
-
val cacheStatusResult = tryFuture {
ScheduleGetCacheStatus(context, it, it)
}
@@ -87,8 +85,6 @@ open class CachedRequest(
if (cacheStatusResult is MyResult.Success) {
val cacheStatus = cacheStatusResult.data
- logger.info("Cache status received successfully!")
-
runBlocking {
repository.setUpdateDates(
cacheStatus.lastCacheUpdate,
@@ -98,12 +94,10 @@ open class CachedRequest(
}
if (cacheStatus.cacheUpdateRequired) {
- logger.info("Cache update was required!")
val updateResult = runBlocking { updateMainPage() }
when (updateResult) {
is MyResult.Success -> {
- logger.info("Cache update was successful!")
runBlocking {
repository.setUpdateDates(
updateResult.data.lastCacheUpdate,
@@ -124,12 +118,10 @@ open class CachedRequest(
val cachedResponse = runBlocking { repository.get(url) }
if (cachedResponse != null) {
- logger.info("Found cached response!")
listener.onResponse(cachedResponse.data)
return
}
- logger.info("Cached response doesn't exists!")
super.send()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/fcm/FcmUpdateCallback.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/fcm/FcmUpdateCallback.kt
new file mode 100644
index 0000000..aefdb45
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/fcm/FcmUpdateCallback.kt
@@ -0,0 +1,18 @@
+package ru.n08i40k.polytechnic.next.network.request.fcm
+
+import android.content.Context
+import com.android.volley.Response
+import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
+
+class FcmUpdateCallback(
+ context: Context,
+ version: String,
+ listener: Response.Listener,
+ errorListener: Response.ErrorListener?,
+) : AuthorizedRequest(
+ context, Method.POST,
+ "fcm/update-callback/$version",
+ { listener.onResponse(Unit) },
+ errorListener,
+ true
+)
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/service/MyFirebaseMessagingService.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/service/MyFirebaseMessagingService.kt
index 72563dd..ed210a8 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/service/MyFirebaseMessagingService.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/service/MyFirebaseMessagingService.kt
@@ -1,8 +1,11 @@
package ru.n08i40k.polytechnic.next.service
import android.Manifest
-import android.content.Context
+import android.app.PendingIntent
+import android.content.Intent
import android.content.pm.PackageManager
+import android.net.Uri
+import androidx.annotation.DrawableRes
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@@ -11,19 +14,12 @@ import androidx.work.Constraints
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
-import androidx.work.Worker
-import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.runBlocking
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.settings.settingsDataStore
+import ru.n08i40k.polytechnic.next.work.FcmSetTokenWorker
import java.time.Duration
class MyFirebaseMessagingService : FirebaseMessagingService() {
@@ -34,7 +30,7 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
- val request = OneTimeWorkRequestBuilder()
+ val request = OneTimeWorkRequestBuilder()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, Duration.ofMinutes(1))
.setInputData(workDataOf("TOKEN" to token))
@@ -45,66 +41,79 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
.enqueue(request)
}
+ private fun sendNotification(
+ channel: String,
+ @DrawableRes iconId: Int,
+ title: String,
+ contentText: String,
+ priority: Int,
+ id: Any?,
+ intent: Intent? = null
+ ) {
+ val pendingIntent: PendingIntent? =
+ if (intent != null)
+ PendingIntent.getActivity(this, 0, intent.apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }, PendingIntent.FLAG_IMMUTABLE)
+ else
+ null
+
+ val notification = NotificationCompat
+ .Builder(applicationContext, channel)
+ .setSmallIcon(iconId)
+ .setContentTitle(title)
+ .setContentText(contentText)
+ .setPriority(priority)
+ .setAutoCancel(true)
+ .setContentIntent(pendingIntent)
+ .build()
+
+ with(NotificationManagerCompat.from(this)) {
+ if (ActivityCompat.checkSelfPermission(
+ this@MyFirebaseMessagingService,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ return@with
+ }
+
+ notify(id.hashCode(), notification)
+ }
+ }
+
override fun onMessageReceived(message: RemoteMessage) {
val type = message.data["type"]
when (type) {
"schedule-update" -> {
- val notification = NotificationCompat
- .Builder(applicationContext, NotificationChannels.SCHEDULE_UPDATE)
- .setSmallIcon(R.drawable.logo)
- .setContentTitle(getString(R.string.schedule_update_title))
- .setContentText(
- getString(
- if (message.data["replaced"] == "true")
- R.string.schedule_update_replaced
- else
- R.string.schedule_update_default
- )
- )
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setAutoCancel(true)
- .build()
+ sendNotification(
+ NotificationChannels.SCHEDULE_UPDATE,
+ R.drawable.logo,
+ getString(R.string.schedule_update_title),
+ getString(
+ if (message.data["replaced"] == "true")
+ R.string.schedule_update_replaced
+ else
+ R.string.schedule_update_default
+ ),
+ NotificationCompat.PRIORITY_DEFAULT,
+ message.data["etag"]
+ )
+ }
- with(NotificationManagerCompat.from(this)) {
- if (ActivityCompat.checkSelfPermission(
- this@MyFirebaseMessagingService,
- Manifest.permission.POST_NOTIFICATIONS
- ) != PackageManager.PERMISSION_GRANTED
- ) {
- return@with
- }
-
- notify(message.data["etag"].hashCode(), notification)
- }
+ "app-update" -> {
+ sendNotification(
+ NotificationChannels.APP_UPDATE,
+ R.drawable.logo,
+ getString(R.string.app_update_title, message.data["version"]),
+ getString(R.string.app_update_description),
+ NotificationCompat.PRIORITY_DEFAULT,
+ message.data["version"],
+ Intent(Intent.ACTION_VIEW, Uri.parse(message.data["downloadLink"]))
+ )
}
}
super.onMessageReceived(message)
}
-
- class SetFcmTokenWorker(context: Context, workerParams: WorkerParameters) :
- Worker(context, workerParams) {
- override fun doWork(): Result {
- val fcmToken = inputData.getString("TOKEN") ?: return Result.failure()
-
- val accessToken = runBlocking {
- applicationContext.settingsDataStore.data.map { it.accessToken }.first()
- }
- if (accessToken.isEmpty())
- return Result.retry()
-
- val setResult = runBlocking {
- (applicationContext as PolytechnicApplication)
- .container
- .profileRepository
- .setFcmToken(fcmToken)
- }
-
- return when (setResult) {
- is MyResult.Success -> Result.success()
- is MyResult.Failure -> Result.retry()
- }
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/MainActivity.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/MainActivity.kt
index 7e03dc1..37b2e5c 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/MainActivity.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/MainActivity.kt
@@ -1,13 +1,12 @@
package ru.n08i40k.polytechnic.next.ui
import android.Manifest
-import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
-import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
+import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
@@ -22,44 +21,76 @@ import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
+import androidx.work.BackoffPolicy
+import androidx.work.Constraints
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
-import androidx.work.Worker
-import androidx.work.WorkerParameters
+import androidx.work.workDataOf
+import com.google.firebase.Firebase
+import com.google.firebase.remoteconfig.FirebaseRemoteConfig
+import com.google.firebase.remoteconfig.remoteConfig
+import com.google.firebase.remoteconfig.remoteConfigSettings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import ru.n08i40k.polytechnic.next.NotificationChannels.SCHEDULE_UPDATE
+import ru.n08i40k.polytechnic.next.NotificationChannels
import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
+import ru.n08i40k.polytechnic.next.work.FcmUpdateCallbackWorker
+import ru.n08i40k.polytechnic.next.work.LinkUpdateWorker
+import java.time.Duration
import java.util.concurrent.TimeUnit
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
- @SuppressLint("ObsoleteSdkInt")
- private fun createNotificationChannel() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val name = getString(R.string.schedule_channel_name)
- val description = getString(R.string.schedule_channel_description)
- val importance = NotificationManager.IMPORTANCE_DEFAULT
- val channel = NotificationChannel(SCHEDULE_UPDATE, name, importance)
- channel.description = description
+ val remoteConfig: FirebaseRemoteConfig = Firebase.remoteConfig
- val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.createNotificationChannel(channel)
- }
+ private val configSettings = remoteConfigSettings {
+ minimumFetchIntervalInSeconds = 3600
}
- private val requestPermissionLauncher = registerForActivityResult(
- ActivityResultContracts.RequestPermission(),
+ private fun createNotificationChannel(
+ notificationManager: NotificationManager,
+ name: String,
+ description: String,
+ channelId: String
) {
- if (it) {
- createNotificationChannel()
- }
+ val channel = NotificationChannel(channelId, name, NotificationManager.IMPORTANCE_DEFAULT)
+ channel.description = description
+
+ notificationManager.createNotificationChannel(channel)
}
+ private fun createNotificationChannels() {
+ if (!hasNotificationPermission())
+ return
+
+ val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+
+ createNotificationChannel(
+ notificationManager,
+ getString(R.string.schedule_channel_name),
+ getString(R.string.schedule_channel_description),
+ NotificationChannels.SCHEDULE_UPDATE
+ )
+
+ createNotificationChannel(
+ notificationManager,
+ getString(R.string.app_update_channel_name),
+ getString(R.string.app_update_channel_description),
+ NotificationChannels.APP_UPDATE
+ )
+ }
+
+ private val requestPermissionLauncher =
+ registerForActivityResult(ActivityResultContracts.RequestPermission()) {
+ if (it) createNotificationChannels()
+ }
+
private fun hasNotificationPermission(): Boolean {
return (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|| ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
@@ -71,42 +102,72 @@ class MainActivity : ComponentActivity() {
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
- class CacheUpdateWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
- override fun doWork(): Result {
- runBlocking {
- (applicationContext as PolytechnicApplication)
- .container
- .scheduleRepository
- .getGroup()
- }
- return Result.success()
- }
- }
- private fun schedulePeriodicRequest() {
+ fun scheduleLinkUpdate(intervalInMinutes: Long) {
+ val tag = "schedule-update"
+
val workRequest = PeriodicWorkRequest.Builder(
- CacheUpdateWorker::class.java,
- 15, TimeUnit.MINUTES
+ LinkUpdateWorker::class.java,
+ intervalInMinutes.coerceAtLeast(15), TimeUnit.MINUTES
)
- .addTag("schedule-update")
+ .addTag(tag)
.build()
val workManager = WorkManager.getInstance(applicationContext)
- workManager.cancelAllWorkByTag("schedule-update")
+ workManager.cancelAllWorkByTag(tag)
workManager.enqueue(workRequest)
}
+ private fun setupFirebaseConfig() {
+ remoteConfig.setConfigSettingsAsync(configSettings)
+ remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
+
+ remoteConfig
+ .fetchAndActivate()
+ .addOnCompleteListener {
+ if (!it.isSuccessful)
+ Log.w("RemoteConfig", "Failed to fetch and activate!")
+
+ scheduleLinkUpdate(remoteConfig.getLong("linkUpdateDelay"))
+ }
+ }
+
+ private fun handleUpdate() {
+ lifecycleScope.launch {
+ val appVersion = (applicationContext as PolytechnicApplication).getAppVersion()
+
+ if (settingsDataStore.data.map { it.version }.first() != appVersion) {
+ settingsDataStore.updateData { it.toBuilder().setVersion(appVersion).build() }
+
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+
+ val request = OneTimeWorkRequestBuilder()
+ .setConstraints(constraints)
+ .setBackoffCriteria(BackoffPolicy.LINEAR, Duration.ofMinutes(1))
+ .setInputData(workDataOf("VERSION" to appVersion))
+ .build()
+
+ WorkManager
+ .getInstance(this@MainActivity)
+ .enqueue(request)
+ }
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
- schedulePeriodicRequest()
askNotificationPermission()
+ createNotificationChannels()
- if (hasNotificationPermission())
- createNotificationChannel()
+ setupFirebaseConfig()
+
+ handleUpdate()
setContent {
Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) {
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/AppIcons.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/AppIcons.kt
new file mode 100644
index 0000000..9e1ceaa
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/AppIcons.kt
@@ -0,0 +1,3 @@
+package ru.n08i40k.polytechnic.next.ui.icons
+
+object AppIcons
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/appicons/Filled.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/appicons/Filled.kt
new file mode 100644
index 0000000..cda0c02
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/appicons/Filled.kt
@@ -0,0 +1,9 @@
+package ru.n08i40k.polytechnic.next.ui.icons.appicons
+
+import ru.n08i40k.polytechnic.next.ui.icons.AppIcons
+
+object FilledGroup
+
+@Suppress("UnusedReceiverParameter")
+val AppIcons.Filled: FilledGroup
+ get() = FilledGroup
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/appicons/filled/Download.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/appicons/filled/Download.kt
new file mode 100644
index 0000000..fbbfe5b
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/appicons/filled/Download.kt
@@ -0,0 +1,59 @@
+package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Round
+import androidx.compose.ui.graphics.StrokeJoin
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup
+
+@Suppress("UnusedReceiverParameter")
+val FilledGroup.Download: ImageVector
+ get() {
+ if (_download != null) {
+ return _download!!
+ }
+ _download = Builder(
+ name = "Download", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp,
+ viewportWidth = 24.0f, viewportHeight = 24.0f
+ ).apply {
+ path(
+ fill = SolidColor(Color(0x00000000)), stroke = SolidColor(Color(0xFF000000)),
+ strokeLineWidth = 2.0f, strokeLineCap = Round, strokeLineJoin =
+ StrokeJoin.Companion.Round, strokeLineMiter = 4.0f, pathFillType = NonZero
+ ) {
+ moveTo(3.0f, 12.3f)
+ verticalLineToRelative(7.0f)
+ arcToRelative(2.0f, 2.0f, 0.0f, false, false, 2.0f, 2.0f)
+ horizontalLineTo(19.0f)
+ arcToRelative(2.0f, 2.0f, 0.0f, false, false, 2.0f, -2.0f)
+ verticalLineToRelative(-7.0f)
+ }
+ path(
+ fill = SolidColor(Color(0x00000000)), stroke = SolidColor(Color(0xFF000000)),
+ strokeLineWidth = 2.0f, strokeLineCap = Round, strokeLineJoin =
+ StrokeJoin.Companion.Round, strokeLineMiter = 4.0f, pathFillType = NonZero
+ ) {
+ moveTo(7.9f, 12.3f)
+ lineToRelative(4.1f, 4.0f)
+ lineToRelative(4.1f, -4.0f)
+ }
+ path(
+ fill = SolidColor(Color(0x00000000)), stroke = SolidColor(Color(0xFF000000)),
+ strokeLineWidth = 2.0f, strokeLineCap = Round, strokeLineJoin =
+ StrokeJoin.Companion.Round, strokeLineMiter = 4.0f, pathFillType = NonZero
+ ) {
+ moveTo(12.0f, 2.7f)
+ lineTo(12.0f, 14.2f)
+ }
+ }
+ .build()
+ return _download!!
+ }
+
+@Suppress("ObjectPropertyName")
+private var _download: ImageVector? = null
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/appicons/filled/Telegram.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/appicons/filled/Telegram.kt
new file mode 100644
index 0000000..5490f6f
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/appicons/filled/Telegram.kt
@@ -0,0 +1,53 @@
+package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup
+
+@Suppress("UnusedReceiverParameter")
+val FilledGroup.Telegram: ImageVector
+ get() {
+ if (_telegram != null) {
+ return _telegram!!
+ }
+ _telegram = Builder(
+ name = "Telegram", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp,
+ viewportWidth = 24.0f, viewportHeight = 24.0f
+ ).apply {
+ path(
+ fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
+ strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
+ pathFillType = NonZero
+ ) {
+ moveToRelative(20.665f, 3.717f)
+ lineToRelative(-17.73f, 6.837f)
+ curveToRelative(-1.21f, 0.486f, -1.203f, 1.161f, -0.222f, 1.462f)
+ lineToRelative(4.552f, 1.42f)
+ lineToRelative(10.532f, -6.645f)
+ curveToRelative(0.498f, -0.303f, 0.953f, -0.14f, 0.579f, 0.192f)
+ lineToRelative(-8.533f, 7.701f)
+ horizontalLineToRelative(-0.002f)
+ lineToRelative(0.002f, 0.001f)
+ lineToRelative(-0.314f, 4.692f)
+ curveToRelative(0.46f, 0.0f, 0.663f, -0.211f, 0.921f, -0.46f)
+ lineToRelative(2.211f, -2.15f)
+ lineToRelative(4.599f, 3.397f)
+ curveToRelative(0.848f, 0.467f, 1.457f, 0.227f, 1.668f, -0.785f)
+ lineToRelative(3.019f, -14.228f)
+ curveToRelative(0.309f, -1.239f, -0.473f, -1.8f, -1.282f, -1.434f)
+ close()
+ }
+ }
+ .build()
+ return _telegram!!
+ }
+
+@Suppress("ObjectPropertyName")
+private var _telegram: ImageVector? = null
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/MainScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/MainScreen.kt
index 8ae780d..5881a17 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/MainScreen.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/MainScreen.kt
@@ -1,25 +1,52 @@
package ru.n08i40k.polytechnic.next.ui.main
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+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.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.Badge
+import androidx.compose.material3.BadgedBox
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat.startActivity
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -32,13 +59,21 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.MainViewModel
+import ru.n08i40k.polytechnic.next.PolytechnicApplication
+import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.model.UserRole
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
+import ru.n08i40k.polytechnic.next.ui.MainActivity
+import ru.n08i40k.polytechnic.next.ui.icons.AppIcons
+import ru.n08i40k.polytechnic.next.ui.icons.appicons.Filled
+import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Download
+import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Telegram
import ru.n08i40k.polytechnic.next.ui.main.profile.ProfileScreen
import ru.n08i40k.polytechnic.next.ui.main.replacer.ReplacerScreen
import ru.n08i40k.polytechnic.next.ui.main.schedule.ScheduleScreen
import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel
+import ru.n08i40k.polytechnic.next.ui.model.RemoteConfigViewModel
import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerViewModel
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
@@ -91,6 +126,88 @@ private fun NavHostContainer(
})
}
+private fun openLink(context: Context, link: String) {
+ startActivity(context, Intent(Intent.ACTION_VIEW, Uri.parse(link)), null)
+}
+
+@Composable
+private fun LinkButton(
+ text: String,
+ icon: ImageVector,
+ link: String,
+ enabled: Boolean = true,
+ badged: Boolean = false,
+) {
+ val context = LocalContext.current
+
+ TextButton(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { openLink(context, link) },
+ enabled = enabled,
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ BadgedBox(badge = { if (badged) Badge() }) {
+ Icon(imageVector = icon, contentDescription = text)
+ }
+ Spacer(Modifier.width(5.dp))
+ Text(text)
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TopNavBar(
+ remoteConfigViewModel: RemoteConfigViewModel
+) {
+ var dropdownExpanded by remember { mutableStateOf(false) }
+ val remoteConfigUiState by remoteConfigViewModel.uiState.collectAsStateWithLifecycle()
+
+ val packageVersion =
+ (LocalContext.current.applicationContext as PolytechnicApplication).getAppVersion()
+ val updateAvailable = remoteConfigUiState.currVersion != packageVersion
+
+ TopAppBar(
+ title = {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(R.string.app_name),
+ style = MaterialTheme.typography.titleLarge,
+ textAlign = TextAlign.Center
+ )
+ },
+ actions = {
+ IconButton(onClick = { dropdownExpanded = true }) {
+ BadgedBox(badge = { if (updateAvailable) Badge() }) {
+ Icon(imageVector = Icons.Filled.Menu, contentDescription = "TopAppBar Menu")
+ }
+ }
+ DropdownMenu(
+ expanded = dropdownExpanded,
+ onDismissRequest = { dropdownExpanded = false }
+ ) {
+ Column(modifier = Modifier.wrapContentWidth()) {
+ LinkButton(
+ text = stringResource(R.string.download_update),
+ icon = AppIcons.Filled.Download,
+ link = remoteConfigUiState.downloadLink,
+ enabled = updateAvailable,
+ badged = updateAvailable
+ )
+ LinkButton(
+ text = stringResource(R.string.telegram_channel),
+ icon = AppIcons.Filled.Telegram,
+ link = remoteConfigUiState.telegramLink,
+ )
+ }
+ }
+ }
+ )
+}
+
@Composable
private fun BottomNavBar(navController: NavHostController, isAdmin: Boolean) {
NavigationBar {
@@ -131,26 +248,49 @@ fun MainScreen(
if (accessToken.isEmpty()) appNavController.navigate("auth")
}
- val scheduleViewModel =
- hiltViewModel(LocalContext.current as ComponentActivity)
-
- LocalContext.current.profileViewModel =
+ // profile view model
+ val profileViewModel: ProfileViewModel =
viewModel(
factory = ProfileViewModel.provideFactory(
profileRepository = mainViewModel.appContainer.profileRepository,
onUnauthorized = { appNavController.navigate("auth") })
)
+ LocalContext.current.profileViewModel = profileViewModel
- val profileUiState by LocalContext.current.profileViewModel!!.uiState.collectAsStateWithLifecycle()
- val isAdmin = (profileUiState is ProfileUiState.HasProfile) &&
- (profileUiState as ProfileUiState.HasProfile).profile.role == UserRole.ADMIN
+ // remote config view model
+ val remoteConfigViewModel: RemoteConfigViewModel =
+ viewModel(
+ factory = RemoteConfigViewModel.provideFactory(
+ appContext = LocalContext.current,
+ remoteConfig = (LocalContext.current as MainActivity).remoteConfig
+ )
+ )
+
+ // schedule view model
+ val scheduleViewModel =
+ hiltViewModel(LocalContext.current as ComponentActivity)
+
+ // schedule replacer view model
+ val profileUiState by profileViewModel.uiState.collectAsStateWithLifecycle()
+
+ val isAdmin = when (profileUiState) {
+ is ProfileUiState.NoProfile -> false
+ is ProfileUiState.HasProfile -> {
+ val profile = (profileUiState as ProfileUiState.HasProfile).profile
+
+ profile.role == UserRole.ADMIN
+ }
+ }
val scheduleReplacerViewModel: ScheduleReplacerViewModel? =
if (isAdmin) hiltViewModel(LocalContext.current as ComponentActivity)
else null
+ // nav controller
+
val navController = rememberNavController()
Scaffold(
+ topBar = { TopNavBar(remoteConfigViewModel) },
bottomBar = { BottomNavBar(navController, isAdmin) }
) { paddingValues ->
NavHostContainer(
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/RemoteConfigViewModel.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/RemoteConfigViewModel.kt
new file mode 100644
index 0000000..22d3eef
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/RemoteConfigViewModel.kt
@@ -0,0 +1,89 @@
+package ru.n08i40k.polytechnic.next.ui.model
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.google.firebase.remoteconfig.ConfigUpdate
+import com.google.firebase.remoteconfig.ConfigUpdateListener
+import com.google.firebase.remoteconfig.FirebaseRemoteConfig
+import com.google.firebase.remoteconfig.FirebaseRemoteConfigException
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import ru.n08i40k.polytechnic.next.ui.MainActivity
+import java.util.logging.Logger
+
+data class RemoteConfigUiState(
+ val minVersion: String,
+ val currVersion: String,
+ val serverVersion: String,
+ val downloadLink: String,
+ val telegramLink: String,
+ val linkUpdateDelay: Long,
+)
+
+
+class RemoteConfigViewModel(
+ private val appContext: Context,
+ private val remoteConfig: FirebaseRemoteConfig,
+) : ViewModel() {
+ private val viewModelState = MutableStateFlow(
+ RemoteConfigUiState(
+ minVersion = remoteConfig.getString("minVersion"),
+ currVersion = remoteConfig.getString("currVersion"),
+ downloadLink = remoteConfig.getString("downloadLink"),
+ telegramLink = remoteConfig.getString("telegramLink"),
+ serverVersion = remoteConfig.getString("serverVersion"),
+ linkUpdateDelay = remoteConfig.getLong("linkUpdateDelay"),
+ )
+ )
+
+ val uiState = viewModelState
+ .stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value)
+
+ init {
+ (appContext as MainActivity)
+ .scheduleLinkUpdate(viewModelState.value.linkUpdateDelay)
+
+ remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener {
+ override fun onUpdate(configUpdate: ConfigUpdate) {
+ remoteConfig.activate().addOnCompleteListener {
+ viewModelState.update {
+ it.copy(
+ minVersion = remoteConfig.getString("minVersion"),
+ currVersion = remoteConfig.getString("currVersion"),
+ downloadLink = remoteConfig.getString("downloadLink"),
+ telegramLink = remoteConfig.getString("telegramLink"),
+ serverVersion = remoteConfig.getString("serverVersion"),
+ linkUpdateDelay = remoteConfig.getLong("linkUpdateDelay"),
+ )
+ }
+
+ appContext.scheduleLinkUpdate(viewModelState.value.linkUpdateDelay)
+ }
+ }
+
+ override fun onError(error: FirebaseRemoteConfigException) {
+ Logger.getLogger("RemoteConfigViewModel")
+ .severe("Failed to fetch RemoteConfig update!")
+ }
+ })
+ }
+
+ companion object {
+ fun provideFactory(
+ appContext: Context,
+ remoteConfig: FirebaseRemoteConfig,
+ ): ViewModelProvider.Factory =
+ object : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ @Suppress("UNCHECKED_CAST") return RemoteConfigViewModel(
+ appContext,
+ remoteConfig,
+ ) as T
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Extensions.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Extensions.kt
new file mode 100644
index 0000000..2208efa
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Extensions.kt
@@ -0,0 +1,7 @@
+package ru.n08i40k.polytechnic.next.utils
+
+infix fun T?.or(data: T): T {
+ if (this == null)
+ return data
+ return this
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/work/FcmSetTokenWorker.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/work/FcmSetTokenWorker.kt
new file mode 100644
index 0000000..6d108e7
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/work/FcmSetTokenWorker.kt
@@ -0,0 +1,36 @@
+package ru.n08i40k.polytechnic.next.work
+
+import android.content.Context
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
+import ru.n08i40k.polytechnic.next.PolytechnicApplication
+import ru.n08i40k.polytechnic.next.data.MyResult
+import ru.n08i40k.polytechnic.next.settings.settingsDataStore
+
+class FcmSetTokenWorker(context: Context, workerParams: WorkerParameters) :
+ Worker(context, workerParams) {
+ override fun doWork(): Result {
+ val fcmToken = inputData.getString("TOKEN") ?: return Result.failure()
+
+ val accessToken = runBlocking {
+ applicationContext.settingsDataStore.data.map { it.accessToken }.first()
+ }
+ if (accessToken.isEmpty())
+ return Result.retry()
+
+ val setResult = runBlocking {
+ (applicationContext as PolytechnicApplication)
+ .container
+ .profileRepository
+ .setFcmToken(fcmToken)
+ }
+
+ return when (setResult) {
+ is MyResult.Success -> Result.success()
+ is MyResult.Failure -> Result.retry()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/work/FcmUpdateCallbackWorker.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/work/FcmUpdateCallbackWorker.kt
new file mode 100644
index 0000000..dd75e79
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/work/FcmUpdateCallbackWorker.kt
@@ -0,0 +1,36 @@
+package ru.n08i40k.polytechnic.next.work
+
+import android.content.Context
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
+import ru.n08i40k.polytechnic.next.data.MyResult
+import ru.n08i40k.polytechnic.next.network.request.fcm.FcmUpdateCallback
+import ru.n08i40k.polytechnic.next.network.tryFuture
+import ru.n08i40k.polytechnic.next.settings.settingsDataStore
+
+class FcmUpdateCallbackWorker(context: Context, workerParams: WorkerParameters) :
+ Worker(context, workerParams) {
+ override fun doWork(): Result {
+ val version = inputData.getString("VERSION") ?: return Result.failure()
+
+ val accessToken = runBlocking {
+ applicationContext.settingsDataStore.data.map { it.accessToken }.first()
+ }
+ if (accessToken.isEmpty())
+ return Result.retry()
+
+ val result = runBlocking {
+ tryFuture {
+ FcmUpdateCallback(this@FcmUpdateCallbackWorker.applicationContext, version, it, it)
+ }
+ }
+
+ return when (result) {
+ is MyResult.Success -> Result.success()
+ is MyResult.Failure -> Result.retry()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/work/LinkUpdateWorker.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/work/LinkUpdateWorker.kt
new file mode 100644
index 0000000..bad1fb4
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/work/LinkUpdateWorker.kt
@@ -0,0 +1,20 @@
+package ru.n08i40k.polytechnic.next.work
+
+import android.content.Context
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.runBlocking
+import ru.n08i40k.polytechnic.next.PolytechnicApplication
+
+class LinkUpdateWorker(context: Context, params: WorkerParameters) :
+ Worker(context, params) {
+ override fun doWork(): Result {
+ runBlocking {
+ (applicationContext as PolytechnicApplication)
+ .container
+ .scheduleRepository
+ .getGroup()
+ }
+ return Result.success()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/proto/settings.proto b/app/src/main/proto/settings.proto
index 3e369ce..d1d4bf7 100644
--- a/app/src/main/proto/settings.proto
+++ b/app/src/main/proto/settings.proto
@@ -19,4 +19,5 @@ message Settings {
string group = 3;
map cache_storage = 4;
UpdateDates update_dates = 5;
+ string version = 6;
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/download.xml b/app/src/main/res/drawable/download.xml
new file mode 100644
index 0000000..0d9bd27
--- /dev/null
+++ b/app/src/main/res/drawable/download.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/telegram.xml b/app/src/main/res/drawable/telegram.xml
new file mode 100644
index 0000000..c773fa8
--- /dev/null
+++ b/app/src/main/res/drawable/telegram.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 0b2ecfb..946186d 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -42,7 +42,13 @@
Загрузить новое расписание
Обновления расписания
Информирует об обновлении расписания
- Расписание обновлено.
+ Расписание обновлено!
Расписание было обновлено Администратором.
Расписание было обновлено на сайте политехникума.
+ Скачать обновление
+ Телеграм канал
+ Обновление приложения
+ Информирует о выходе новой версии этого приложения
+ Вышла версия %1$s!
+ Нажмите что бы загрузить обновление.
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fe10ae3..dcff827 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -42,7 +42,13 @@
Set new
Schedule update
Inform when schedule has been updated
- Schedule has been updated.
+ Schedule has been updated!
Schedule was updated by Administrator.
Schedule was updated on polytechnic website.
+ Download update
+ Telegram channel
+ Application update
+ Inform about new version of this app has been released
+ Version %1$s released!
+ Click to download new version.
\ No newline at end of file
diff --git a/app/src/main/res/xml/remote_config_defaults.xml b/app/src/main/res/xml/remote_config_defaults.xml
new file mode 100644
index 0000000..33683e0
--- /dev/null
+++ b/app/src/main/res/xml/remote_config_defaults.xml
@@ -0,0 +1,27 @@
+
+
+
+ linkUpdateDelay
+ 15
+
+
+ minVersion
+ 1.3.0
+
+
+ currVersion
+ 1.5.0
+
+
+ downloadLink
+ https://t.me/polytechnic_next/16
+
+
+ telegramLink
+ https://t.me/polytechnic_next
+
+
+ serverVersion
+ 1.3.0
+
+
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ad36080..1f3678b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
[versions]
accompanistSwiperefresh = "0.36.0"
-agp = "8.6.1"
+agp = "8.7.0"
firebaseBom = "33.4.0"
hiltAndroid = "2.52"
hiltAndroidCompiler = "2.52"
@@ -18,9 +18,7 @@ protobufLite = "3.0.1"
volley = "1.2.1"
datastore = "1.1.1"
navigationCompose = "2.8.2"
-firebaseCrashlytics = "19.2.0"
googleFirebaseCrashlytics = "3.0.2"
-firebaseMessaging = "24.0.2"
workRuntime = "2.9.1"
[libraries]
@@ -29,8 +27,6 @@ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref =
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "workRuntime" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntime" }
-firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
-firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
@@ -51,8 +47,11 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
protobuf-lite = { module = "com.google.protobuf:protobuf-lite", version.ref = "protobufLite" }
volley = { group = "com.android.volley", name = "volley", version.ref = "volley" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
-firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics", version.ref = "firebaseCrashlytics" }
-firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging", version.ref = "firebaseMessaging" }
+firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
+firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
+firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" }
+firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" }
+firebase-config = { group = "com.google.firebase", name = "firebase-config" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 1a34ebf..e40c8b5 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Sat Sep 07 22:06:56 GMT+04:00 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists