mirror of
https://github.com/n08i40k/polytechnic-android.git
synced 2025-12-06 09:47:48 +03:00
1.6.0
Поддержка Firebase Remote Config. Уведомления об обновлении приложения. Добавлен TopBar с проверкой обновлений. Удалены некоторые уже не важные логи.
This commit is contained in:
27
.idea/appInsightsSettings.xml
generated
27
.idea/appInsightsSettings.xml
generated
@@ -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
2
.idea/compiler.xml
generated
@@ -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
1
.idea/gradle.xml
generated
@@ -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
2
.idea/misc.xml
generated
@@ -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">
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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))) {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package ru.n08i40k.polytechnic.next.ui.icons
|
||||||
|
|
||||||
|
object AppIcons
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
9
app/src/main/res/drawable/download.xml
Normal file
9
app/src/main/res/drawable/download.xml
Normal 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>
|
||||||
5
app/src/main/res/drawable/telegram.xml
Normal file
5
app/src/main/res/drawable/telegram.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
27
app/src/main/res/xml/remote_config_defaults.xml
Normal file
27
app/src/main/res/xml/remote_config_defaults.xml
Normal 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>
|
||||||
@@ -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" }
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user