Поддержка Firebase Remote Config.

Уведомления об обновлении приложения.

Добавлен TopBar с проверкой обновлений.

Удалены некоторые уже не важные логи.
This commit is contained in:
2024-10-06 02:56:18 +04:00
parent 1c773b4713
commit 43cb667614
30 changed files with 728 additions and 159 deletions

View File

@@ -15,35 +15,8 @@
<option name="projectNumber" value="946974192625" /> <option name="projectNumber" value="946974192625" />
</ConnectionSetting> </ConnectionSetting>
</option> </option>
<option name="devices">
<list />
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" /> <option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" /> <option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="versions">
<list>
<VersionSetting>
<option name="buildVersion" value="9" />
<option name="displayName" value="1.4.0 (9)" />
<option name="displayVersion" value="1.4.0" />
</VersionSetting>
<VersionSetting>
<option name="buildVersion" value="8" />
<option name="displayName" value="1.3.2 (8)" />
<option name="displayVersion" value="1.3.2" />
</VersionSetting>
<VersionSetting>
<option name="buildVersion" value="7" />
<option name="displayName" value="1.3.1 (7)" />
<option name="displayVersion" value="1.3.1" />
</VersionSetting>
<VersionSetting>
<option name="buildVersion" value="6" />
<option name="displayName" value="1.3.0 (6)" />
<option name="displayVersion" value="1.3.0" />
</VersionSetting>
</list>
</option>
<option name="visibilityType" value="ALL" /> <option name="visibilityType" value="ALL" />
</InsightsFilterSettings> </InsightsFilterSettings>
</value> </value>

2
.idea/compiler.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="CompilerConfiguration"> <component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" /> <bytecodeTargetLevel target="21" />
</component> </component>
</project> </project>

1
.idea/gradle.xml generated
View File

@@ -4,6 +4,7 @@
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" /> <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules"> <option name="modules">

2
.idea/misc.xml generated
View File

@@ -7,7 +7,7 @@
</list> </list>
</component> </component>
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View File

@@ -32,8 +32,8 @@ android {
applicationId = "ru.n08i40k.polytechnic.next" applicationId = "ru.n08i40k.polytechnic.next"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 10 versionCode = 11
versionName = "1.5.0" versionName = "1.6.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@@ -103,6 +103,7 @@ dependencies {
implementation(libs.firebase.analytics) implementation(libs.firebase.analytics)
implementation(libs.firebase.messaging) implementation(libs.firebase.messaging)
implementation(libs.firebase.crashlytics) implementation(libs.firebase.crashlytics)
implementation(libs.firebase.config)
// datastore // datastore
implementation(libs.androidx.datastore) implementation(libs.androidx.datastore)

View File

@@ -2,4 +2,5 @@ package ru.n08i40k.polytechnic.next
object NotificationChannels { object NotificationChannels {
const val SCHEDULE_UPDATE = "schedule-update" const val SCHEDULE_UPDATE = "schedule-update"
const val APP_UPDATE = "app-update"
} }

View File

@@ -3,6 +3,7 @@ package ru.n08i40k.polytechnic.next
import android.app.Application import android.app.Application
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import ru.n08i40k.polytechnic.next.data.AppContainer import ru.n08i40k.polytechnic.next.data.AppContainer
import ru.n08i40k.polytechnic.next.utils.or
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidApp @HiltAndroidApp
@@ -10,4 +11,10 @@ class PolytechnicApplication : Application() {
@Suppress("unused") @Suppress("unused")
@Inject @Inject
lateinit var container: AppContainer lateinit var container: AppContainer
fun getAppVersion(): String {
return applicationContext.packageManager
.getPackageInfo(this.packageName, 0)
.versionName or "1.0.0"
}
} }

View File

@@ -30,7 +30,8 @@ open class AuthorizedRequest(
.setAccessToken("").build() .setAccessToken("").build()
} }
} }
context.profileViewModel!!.onUnauthorized() if (context.profileViewModel != null)
context.profileViewModel!!.onUnauthorized()
} }
errorListener?.onErrorResponse(it) errorListener?.onErrorResponse(it)

View File

@@ -78,8 +78,6 @@ open class CachedRequest(
val logger = Logger.getLogger("CachedRequest") val logger = Logger.getLogger("CachedRequest")
val repository = appContainer.networkCacheRepository val repository = appContainer.networkCacheRepository
logger.info("Getting cache status...")
val cacheStatusResult = tryFuture { val cacheStatusResult = tryFuture {
ScheduleGetCacheStatus(context, it, it) ScheduleGetCacheStatus(context, it, it)
} }
@@ -87,8 +85,6 @@ open class CachedRequest(
if (cacheStatusResult is MyResult.Success) { if (cacheStatusResult is MyResult.Success) {
val cacheStatus = cacheStatusResult.data val cacheStatus = cacheStatusResult.data
logger.info("Cache status received successfully!")
runBlocking { runBlocking {
repository.setUpdateDates( repository.setUpdateDates(
cacheStatus.lastCacheUpdate, cacheStatus.lastCacheUpdate,
@@ -98,12 +94,10 @@ open class CachedRequest(
} }
if (cacheStatus.cacheUpdateRequired) { if (cacheStatus.cacheUpdateRequired) {
logger.info("Cache update was required!")
val updateResult = runBlocking { updateMainPage() } val updateResult = runBlocking { updateMainPage() }
when (updateResult) { when (updateResult) {
is MyResult.Success -> { is MyResult.Success -> {
logger.info("Cache update was successful!")
runBlocking { runBlocking {
repository.setUpdateDates( repository.setUpdateDates(
updateResult.data.lastCacheUpdate, updateResult.data.lastCacheUpdate,
@@ -124,12 +118,10 @@ open class CachedRequest(
val cachedResponse = runBlocking { repository.get(url) } val cachedResponse = runBlocking { repository.get(url) }
if (cachedResponse != null) { if (cachedResponse != null) {
logger.info("Found cached response!")
listener.onResponse(cachedResponse.data) listener.onResponse(cachedResponse.data)
return return
} }
logger.info("Cached response doesn't exists!")
super.send() super.send()
} }
} }

View File

@@ -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<Unit>,
errorListener: Response.ErrorListener?,
) : AuthorizedRequest(
context, Method.POST,
"fcm/update-callback/$version",
{ listener.onResponse(Unit) },
errorListener,
true
)

View File

@@ -1,8 +1,11 @@
package ru.n08i40k.polytechnic.next.service package ru.n08i40k.polytechnic.next.service
import android.Manifest import android.Manifest
import android.content.Context import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import androidx.annotation.DrawableRes
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@@ -11,19 +14,12 @@ import androidx.work.Constraints
import androidx.work.NetworkType import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
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.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.NotificationChannels import ru.n08i40k.polytechnic.next.NotificationChannels
import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.work.FcmSetTokenWorker
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
import java.time.Duration import java.time.Duration
class MyFirebaseMessagingService : FirebaseMessagingService() { class MyFirebaseMessagingService : FirebaseMessagingService() {
@@ -34,7 +30,7 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
.setRequiredNetworkType(NetworkType.CONNECTED) .setRequiredNetworkType(NetworkType.CONNECTED)
.build() .build()
val request = OneTimeWorkRequestBuilder<SetFcmTokenWorker>() val request = OneTimeWorkRequestBuilder<FcmSetTokenWorker>()
.setConstraints(constraints) .setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, Duration.ofMinutes(1)) .setBackoffCriteria(BackoffPolicy.LINEAR, Duration.ofMinutes(1))
.setInputData(workDataOf("TOKEN" to token)) .setInputData(workDataOf("TOKEN" to token))
@@ -45,66 +41,79 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
.enqueue(request) .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) { override fun onMessageReceived(message: RemoteMessage) {
val type = message.data["type"] val type = message.data["type"]
when (type) { when (type) {
"schedule-update" -> { "schedule-update" -> {
val notification = NotificationCompat sendNotification(
.Builder(applicationContext, NotificationChannels.SCHEDULE_UPDATE) NotificationChannels.SCHEDULE_UPDATE,
.setSmallIcon(R.drawable.logo) R.drawable.logo,
.setContentTitle(getString(R.string.schedule_update_title)) getString(R.string.schedule_update_title),
.setContentText( getString(
getString( if (message.data["replaced"] == "true")
if (message.data["replaced"] == "true") R.string.schedule_update_replaced
R.string.schedule_update_replaced else
else R.string.schedule_update_default
R.string.schedule_update_default ),
) NotificationCompat.PRIORITY_DEFAULT,
) message.data["etag"]
.setPriority(NotificationCompat.PRIORITY_DEFAULT) )
.setAutoCancel(true) }
.build()
with(NotificationManagerCompat.from(this)) { "app-update" -> {
if (ActivityCompat.checkSelfPermission( sendNotification(
this@MyFirebaseMessagingService, NotificationChannels.APP_UPDATE,
Manifest.permission.POST_NOTIFICATIONS R.drawable.logo,
) != PackageManager.PERMISSION_GRANTED getString(R.string.app_update_title, message.data["version"]),
) { getString(R.string.app_update_description),
return@with NotificationCompat.PRIORITY_DEFAULT,
} message.data["version"],
Intent(Intent.ACTION_VIEW, Uri.parse(message.data["downloadLink"]))
notify(message.data["etag"].hashCode(), notification) )
}
} }
} }
super.onMessageReceived(message) 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()
}
}
}
} }

View File

@@ -1,13 +1,12 @@
package ru.n08i40k.polytechnic.next.ui package ru.n08i40k.polytechnic.next.ui
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
@@ -22,44 +21,76 @@ import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequest import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.Worker import androidx.work.workDataOf
import androidx.work.WorkerParameters 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 dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import ru.n08i40k.polytechnic.next.NotificationChannels
import ru.n08i40k.polytechnic.next.NotificationChannels.SCHEDULE_UPDATE
import ru.n08i40k.polytechnic.next.PolytechnicApplication import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.settings.settingsDataStore 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 import java.util.concurrent.TimeUnit
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@SuppressLint("ObsoleteSdkInt") val remoteConfig: FirebaseRemoteConfig = Firebase.remoteConfig
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 notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager private val configSettings = remoteConfigSettings {
notificationManager.createNotificationChannel(channel) minimumFetchIntervalInSeconds = 3600
}
} }
private val requestPermissionLauncher = registerForActivityResult( private fun createNotificationChannel(
ActivityResultContracts.RequestPermission(), notificationManager: NotificationManager,
name: String,
description: String,
channelId: String
) { ) {
if (it) { val channel = NotificationChannel(channelId, name, NotificationManager.IMPORTANCE_DEFAULT)
createNotificationChannel() 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 { private fun hasNotificationPermission(): Boolean {
return (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU return (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|| ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) || ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
@@ -71,42 +102,72 @@ class MainActivity : ComponentActivity() {
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) 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( val workRequest = PeriodicWorkRequest.Builder(
CacheUpdateWorker::class.java, LinkUpdateWorker::class.java,
15, TimeUnit.MINUTES intervalInMinutes.coerceAtLeast(15), TimeUnit.MINUTES
) )
.addTag("schedule-update") .addTag(tag)
.build() .build()
val workManager = WorkManager.getInstance(applicationContext) val workManager = WorkManager.getInstance(applicationContext)
workManager.cancelAllWorkByTag("schedule-update") workManager.cancelAllWorkByTag(tag)
workManager.enqueue(workRequest) 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<FcmUpdateCallbackWorker>()
.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?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
schedulePeriodicRequest()
askNotificationPermission() askNotificationPermission()
createNotificationChannels()
if (hasNotificationPermission()) setupFirebaseConfig()
createNotificationChannel()
handleUpdate()
setContent { setContent {
Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) { Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) {

View File

@@ -0,0 +1,3 @@
package ru.n08i40k.polytechnic.next.ui.icons
object AppIcons

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,25 +1,52 @@
package ru.n08i40k.polytechnic.next.ui.main 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.activity.ComponentActivity
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.slideIn import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut import androidx.compose.animation.slideOut
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues 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.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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntOffset 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.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -32,13 +59,21 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.MainViewModel 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.model.UserRole
import ru.n08i40k.polytechnic.next.settings.settingsDataStore 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.profile.ProfileScreen
import ru.n08i40k.polytechnic.next.ui.main.replacer.ReplacerScreen import ru.n08i40k.polytechnic.next.ui.main.replacer.ReplacerScreen
import ru.n08i40k.polytechnic.next.ui.main.schedule.ScheduleScreen import ru.n08i40k.polytechnic.next.ui.main.schedule.ScheduleScreen
import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel 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.ScheduleReplacerViewModel
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
import ru.n08i40k.polytechnic.next.ui.model.profileViewModel 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 @Composable
private fun BottomNavBar(navController: NavHostController, isAdmin: Boolean) { private fun BottomNavBar(navController: NavHostController, isAdmin: Boolean) {
NavigationBar { NavigationBar {
@@ -131,26 +248,49 @@ fun MainScreen(
if (accessToken.isEmpty()) appNavController.navigate("auth") if (accessToken.isEmpty()) appNavController.navigate("auth")
} }
val scheduleViewModel = // profile view model
hiltViewModel<ScheduleViewModel>(LocalContext.current as ComponentActivity) val profileViewModel: ProfileViewModel =
LocalContext.current.profileViewModel =
viewModel( viewModel(
factory = ProfileViewModel.provideFactory( factory = ProfileViewModel.provideFactory(
profileRepository = mainViewModel.appContainer.profileRepository, profileRepository = mainViewModel.appContainer.profileRepository,
onUnauthorized = { appNavController.navigate("auth") }) onUnauthorized = { appNavController.navigate("auth") })
) )
LocalContext.current.profileViewModel = profileViewModel
val profileUiState by LocalContext.current.profileViewModel!!.uiState.collectAsStateWithLifecycle() // remote config view model
val isAdmin = (profileUiState is ProfileUiState.HasProfile) && val remoteConfigViewModel: RemoteConfigViewModel =
(profileUiState as ProfileUiState.HasProfile).profile.role == UserRole.ADMIN viewModel(
factory = RemoteConfigViewModel.provideFactory(
appContext = LocalContext.current,
remoteConfig = (LocalContext.current as MainActivity).remoteConfig
)
)
// schedule view model
val scheduleViewModel =
hiltViewModel<ScheduleViewModel>(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? = val scheduleReplacerViewModel: ScheduleReplacerViewModel? =
if (isAdmin) hiltViewModel(LocalContext.current as ComponentActivity) if (isAdmin) hiltViewModel(LocalContext.current as ComponentActivity)
else null else null
// nav controller
val navController = rememberNavController() val navController = rememberNavController()
Scaffold( Scaffold(
topBar = { TopNavBar(remoteConfigViewModel) },
bottomBar = { BottomNavBar(navController, isAdmin) } bottomBar = { BottomNavBar(navController, isAdmin) }
) { paddingValues -> ) { paddingValues ->
NavHostContainer( NavHostContainer(

View File

@@ -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 <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST") return RemoteConfigViewModel(
appContext,
remoteConfig,
) as T
}
}
}
}

View File

@@ -0,0 +1,7 @@
package ru.n08i40k.polytechnic.next.utils
infix fun <T> T?.or(data: T): T {
if (this == null)
return data
return this
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}

View File

@@ -19,4 +19,5 @@ message Settings {
string group = 3; string group = 3;
map<string, CachedResponse> cache_storage = 4; map<string, CachedResponse> cache_storage = 4;
UpdateDates update_dates = 5; UpdateDates update_dates = 5;
string version = 6;
} }

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#00000000" android:pathData="M3,12.3v7a2,2 0,0 0,2 2H19a2,2 0,0 0,2 -2v-7" android:strokeColor="#000000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000" android:pathData="M7.9,12.3l4.1,4l4.1,-4" android:strokeColor="#000000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000" android:pathData="M12,2.7L12,14.2" android:strokeColor="#000000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#000000" android:pathData="m20.665,3.717 l-17.73,6.837c-1.21,0.486 -1.203,1.161 -0.222,1.462l4.552,1.42 10.532,-6.645c0.498,-0.303 0.953,-0.14 0.579,0.192l-8.533,7.701h-0.002l0.002,0.001 -0.314,4.692c0.46,0 0.663,-0.211 0.921,-0.46l2.211,-2.15 4.599,3.397c0.848,0.467 1.457,0.227 1.668,-0.785l3.019,-14.228c0.309,-1.239 -0.473,-1.8 -1.282,-1.434z"/>
</vector>

View File

@@ -42,7 +42,13 @@
<string name="set_replacer">Загрузить новое расписание</string> <string name="set_replacer">Загрузить новое расписание</string>
<string name="schedule_channel_name">Обновления расписания</string> <string name="schedule_channel_name">Обновления расписания</string>
<string name="schedule_channel_description">Информирует об обновлении расписания</string> <string name="schedule_channel_description">Информирует об обновлении расписания</string>
<string name="schedule_update_title">Расписание обновлено.</string> <string name="schedule_update_title">Расписание обновлено!</string>
<string name="schedule_update_replaced">Расписание было обновлено Администратором.</string> <string name="schedule_update_replaced">Расписание было обновлено Администратором.</string>
<string name="schedule_update_default">Расписание было обновлено на сайте политехникума.</string> <string name="schedule_update_default">Расписание было обновлено на сайте политехникума.</string>
<string name="download_update">Скачать обновление</string>
<string name="telegram_channel">Телеграм канал</string>
<string name="app_update_channel_name">Обновление приложения</string>
<string name="app_update_channel_description">Информирует о выходе новой версии этого приложения</string>
<string name="app_update_title">Вышла версия %1$s!</string>
<string name="app_update_description">Нажмите что бы загрузить обновление.</string>
</resources> </resources>

View File

@@ -42,7 +42,13 @@
<string name="set_replacer">Set new</string> <string name="set_replacer">Set new</string>
<string name="schedule_channel_name">Schedule update</string> <string name="schedule_channel_name">Schedule update</string>
<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="schedule_update_title">Schedule has been updated.</string> <string name="schedule_update_title">Schedule has been updated!</string>
<string name="schedule_update_replaced">Schedule was updated by Administrator.</string> <string name="schedule_update_replaced">Schedule was updated by Administrator.</string>
<string name="schedule_update_default">Schedule was updated on polytechnic website.</string> <string name="schedule_update_default">Schedule was updated on polytechnic website.</string>
<string name="download_update">Download update</string>
<string name="telegram_channel">Telegram channel</string>
<string name="app_update_channel_name">Application update</string>
<string name="app_update_channel_description">Inform about new version of this app has been released</string>
<string name="app_update_title">Version %1$s released!</string>
<string name="app_update_description">Click to download new version.</string>
</resources> </resources>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<defaults>
<entry>
<key>linkUpdateDelay</key>
<value>15</value>
</entry>
<entry>
<key>minVersion</key>
<value>1.3.0</value>
</entry>
<entry>
<key>currVersion</key>
<value>1.5.0</value>
</entry>
<entry>
<key>downloadLink</key>
<value>https://t.me/polytechnic_next/16</value>
</entry>
<entry>
<key>telegramLink</key>
<value>https://t.me/polytechnic_next</value>
</entry>
<entry>
<key>serverVersion</key>
<value>1.3.0</value>
</entry>
</defaults>

View File

@@ -1,6 +1,6 @@
[versions] [versions]
accompanistSwiperefresh = "0.36.0" accompanistSwiperefresh = "0.36.0"
agp = "8.6.1" agp = "8.7.0"
firebaseBom = "33.4.0" firebaseBom = "33.4.0"
hiltAndroid = "2.52" hiltAndroid = "2.52"
hiltAndroidCompiler = "2.52" hiltAndroidCompiler = "2.52"
@@ -18,9 +18,7 @@ protobufLite = "3.0.1"
volley = "1.2.1" volley = "1.2.1"
datastore = "1.1.1" datastore = "1.1.1"
navigationCompose = "2.8.2" navigationCompose = "2.8.2"
firebaseCrashlytics = "19.2.0"
googleFirebaseCrashlytics = "3.0.2" googleFirebaseCrashlytics = "3.0.2"
firebaseMessaging = "24.0.2"
workRuntime = "2.9.1" workRuntime = "2.9.1"
[libraries] [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-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 = { module = "androidx.work:work-runtime", version.ref = "workRuntime" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", 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-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
junit = { group = "junit", name = "junit", version.ref = "junit" } 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" } protobuf-lite = { module = "com.google.protobuf:protobuf-lite", version.ref = "protobufLite" }
volley = { group = "com.android.volley", name = "volley", version.ref = "volley" } volley = { group = "com.android.volley", name = "volley", version.ref = "volley" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } 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-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging", version.ref = "firebaseMessaging" } 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] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }

View File

@@ -1,6 +1,6 @@
#Sat Sep 07 22:06:56 GMT+04:00 2024 #Sat Sep 07 22:06:56 GMT+04:00 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists