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

Обновление кеша сервера в фоновом режиме (раз в 15 минут).

Теперь полный порядок с запросами.
This commit is contained in:
2024-10-05 00:27:43 +04:00
parent 13d77bbf28
commit 800c49dcc4
60 changed files with 657 additions and 374 deletions

View File

@@ -15,15 +15,33 @@
<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"> <option name="versions">
<list> <list>
<VersionSetting>
<option name="buildVersion" value="9" />
<option name="displayName" value="1.4.0 (9)" />
<option name="displayVersion" value="1.4.0" />
</VersionSetting>
<VersionSetting> <VersionSetting>
<option name="buildVersion" value="8" /> <option name="buildVersion" value="8" />
<option name="displayName" value="1.3.2 (8)" /> <option name="displayName" value="1.3.2 (8)" />
<option name="displayVersion" value="1.3.2" /> <option name="displayVersion" value="1.3.2" />
</VersionSetting> </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> </list>
</option> </option>
<option name="visibilityType" value="ALL" /> <option name="visibilityType" value="ALL" />

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 = 9 versionCode = 10
versionName = "1.4.0" versionName = "1.5.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@@ -89,13 +89,22 @@ android {
} }
dependencies { dependencies {
// work manager
implementation(libs.androidx.work.runtime)
implementation(libs.androidx.work.runtime.ktx)
// hilt
implementation(libs.hilt.android) implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler) ksp(libs.hilt.android.compiler)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)
// firebase
implementation(platform(libs.firebase.bom)) implementation(platform(libs.firebase.bom))
implementation(libs.firebase.analytics) implementation(libs.firebase.analytics)
implementation(libs.firebase.messaging)
implementation(libs.firebase.crashlytics)
// datastore
implementation(libs.androidx.datastore) implementation(libs.androidx.datastore)
implementation(libs.protobuf.lite) implementation(libs.protobuf.lite)
@@ -112,7 +121,7 @@ dependencies {
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.volley) implementation(libs.volley)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.firebase.crashlytics)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:name=".PolytechnicApplication" android:name=".PolytechnicApplication"
@@ -14,6 +15,15 @@
android:roundIcon="@drawable/ic_launcher_round" android:roundIcon="@drawable/ic_launcher_round"
android:theme="@style/Theme.PolytechnicNext" android:theme="@style/Theme.PolytechnicNext"
tools:targetApi="35"> tools:targetApi="35">
<service
android:name=".service.MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<activity <activity
android:name=".ui.MainActivity" android:name=".ui.MainActivity"
android:exported="true" android:exported="true"

View File

@@ -0,0 +1,5 @@
package ru.n08i40k.polytechnic.next
object NotificationChannels {
const val SCHEDULE_UPDATE = "schedule-update"
}

View File

@@ -10,9 +10,9 @@ import ru.n08i40k.polytechnic.next.data.cache.NetworkCacheRepository
import ru.n08i40k.polytechnic.next.data.cache.impl.FakeNetworkCacheRepository import ru.n08i40k.polytechnic.next.data.cache.impl.FakeNetworkCacheRepository
import ru.n08i40k.polytechnic.next.data.cache.impl.LocalNetworkCacheRepository import ru.n08i40k.polytechnic.next.data.cache.impl.LocalNetworkCacheRepository
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleReplacerRepository import ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl.FakeScheduleReplacerRepository
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
import ru.n08i40k.polytechnic.next.data.schedule.impl.RemoteScheduleReplacerRepository import ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl.RemoteScheduleReplacerRepository
import ru.n08i40k.polytechnic.next.data.schedule.impl.RemoteScheduleRepository import ru.n08i40k.polytechnic.next.data.schedule.impl.RemoteScheduleRepository
import ru.n08i40k.polytechnic.next.data.scheduleReplacer.ScheduleReplacerRepository import ru.n08i40k.polytechnic.next.data.scheduleReplacer.ScheduleReplacerRepository
import ru.n08i40k.polytechnic.next.data.users.ProfileRepository import ru.n08i40k.polytechnic.next.data.users.ProfileRepository

View File

@@ -9,14 +9,11 @@ import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.model.Group import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetReq import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGet
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetReqData
import ru.n08i40k.polytechnic.next.network.tryFuture import ru.n08i40k.polytechnic.next.network.tryFuture
import ru.n08i40k.polytechnic.next.settings.settingsDataStore import ru.n08i40k.polytechnic.next.settings.settingsDataStore
class RemoteScheduleRepository(private val context: Context) : ScheduleRepository { class RemoteScheduleRepository(private val context: Context) : ScheduleRepository {
override suspend fun getGroup(): MyResult<Group> { override suspend fun getGroup(): MyResult<Group> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val groupName = runBlocking { val groupName = runBlocking {
@@ -27,8 +24,8 @@ class RemoteScheduleRepository(private val context: Context) : ScheduleRepositor
return@withContext MyResult.Failure(IllegalArgumentException("No group name provided!")) return@withContext MyResult.Failure(IllegalArgumentException("No group name provided!"))
val response = tryFuture { val response = tryFuture {
ScheduleGetReq( ScheduleGet(
ScheduleGetReqData(groupName), ScheduleGet.RequestDto(groupName),
context, context,
it, it,
it it

View File

@@ -1,4 +1,4 @@
package ru.n08i40k.polytechnic.next.data.schedule.impl package ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl
import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.scheduleReplacer.ScheduleReplacerRepository import ru.n08i40k.polytechnic.next.data.scheduleReplacer.ScheduleReplacerRepository

View File

@@ -1,4 +1,4 @@
package ru.n08i40k.polytechnic.next.data.schedule.impl package ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl
import android.content.Context import android.content.Context
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -6,15 +6,15 @@ import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.scheduleReplacer.ScheduleReplacerRepository import ru.n08i40k.polytechnic.next.data.scheduleReplacer.ScheduleReplacerRepository
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
import ru.n08i40k.polytechnic.next.network.data.scheduleReplacer.ScheduleReplacerClearReq import ru.n08i40k.polytechnic.next.network.request.scheduleReplacer.ScheduleReplacerClear
import ru.n08i40k.polytechnic.next.network.data.scheduleReplacer.ScheduleReplacerGetReq import ru.n08i40k.polytechnic.next.network.request.scheduleReplacer.ScheduleReplacerGet
import ru.n08i40k.polytechnic.next.network.data.scheduleReplacer.ScheduleReplacerSetReq import ru.n08i40k.polytechnic.next.network.request.scheduleReplacer.ScheduleReplacerSet
import ru.n08i40k.polytechnic.next.network.tryFuture import ru.n08i40k.polytechnic.next.network.tryFuture
class RemoteScheduleReplacerRepository(private val context: Context) : ScheduleReplacerRepository { class RemoteScheduleReplacerRepository(private val context: Context) : ScheduleReplacerRepository {
override suspend fun getAll(): MyResult<List<ScheduleReplacer>> = override suspend fun getAll(): MyResult<List<ScheduleReplacer>> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
tryFuture { ScheduleReplacerGetReq(context, it, it) } tryFuture { ScheduleReplacerGet(context, it, it) }
} }
@@ -24,12 +24,12 @@ class RemoteScheduleReplacerRepository(private val context: Context) : ScheduleR
fileType: String fileType: String
): MyResult<Nothing> = ): MyResult<Nothing> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
tryFuture { ScheduleReplacerSetReq(context, fileName, fileData, fileType, it, it) } tryFuture { ScheduleReplacerSet(context, fileName, fileData, fileType, it, it) }
} }
override suspend fun clear(): MyResult<Int> { override suspend fun clear(): MyResult<Int> {
val response = withContext(Dispatchers.IO) { val response = withContext(Dispatchers.IO) {
tryFuture { ScheduleReplacerClearReq(context, it, it) } tryFuture { ScheduleReplacerClear(context, it, it) }
} }
return when (response) { return when (response) {

View File

@@ -5,4 +5,6 @@ import ru.n08i40k.polytechnic.next.model.Profile
interface ProfileRepository { interface ProfileRepository {
suspend fun getProfile(): MyResult<Profile> suspend fun getProfile(): MyResult<Profile>
suspend fun setFcmToken(token: String): MyResult<Unit>
} }

View File

@@ -26,4 +26,8 @@ class FakeProfileRepository : ProfileRepository {
MyResult.Success(exampleProfile) MyResult.Success(exampleProfile)
} }
} }
override suspend fun setFcmToken(token: String): MyResult<Unit> {
return MyResult.Success(Unit)
}
} }

View File

@@ -6,18 +6,18 @@ import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.users.ProfileRepository import ru.n08i40k.polytechnic.next.data.users.ProfileRepository
import ru.n08i40k.polytechnic.next.model.Profile import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.network.data.profile.UsersMeRequest import ru.n08i40k.polytechnic.next.network.request.fcm.FcmSetToken
import ru.n08i40k.polytechnic.next.network.request.profile.ProfileMe
import ru.n08i40k.polytechnic.next.network.tryFuture import ru.n08i40k.polytechnic.next.network.tryFuture
class RemoteProfileRepository(private val context: Context) : ProfileRepository { class RemoteProfileRepository(private val context: Context) : ProfileRepository {
override suspend fun getProfile(): MyResult<Profile> = override suspend fun getProfile(): MyResult<Profile> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
tryFuture { tryFuture { ProfileMe(context, it, it) }
UsersMeRequest( }
context,
it, override suspend fun setFcmToken(token: String): MyResult<Unit> =
it withContext(Dispatchers.IO) {
) tryFuture { FcmSetToken(context, token, it, it) }
}
} }
} }

View File

@@ -2,7 +2,7 @@ package ru.n08i40k.polytechnic.next.network
import android.content.Context import android.content.Context
import com.android.volley.Response import com.android.volley.Response
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.DataOutputStream import java.io.DataOutputStream

View File

@@ -1,5 +1,5 @@
package ru.n08i40k.polytechnic.next.network package ru.n08i40k.polytechnic.next.network
object NetworkValues { object NetworkValues {
const val API_HOST = "https://polytechnic.n08i40k.ru:5050/api/v1/" const val API_HOST = "https://192.168.0.103:5050/api/v1/"
} }

View File

@@ -10,7 +10,9 @@ fun <ResultT, RequestT : RequestBase> tryFuture(
buildRequest: (RequestFuture<ResultT>) -> RequestT buildRequest: (RequestFuture<ResultT>) -> RequestT
): MyResult<ResultT> { ): MyResult<ResultT> {
val future = RequestFuture.newFuture<ResultT>() val future = RequestFuture.newFuture<ResultT>()
buildRequest(future).send() buildRequest(future).send()
return tryGet(future) return tryGet(future)
} }

View File

@@ -1,6 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.auth
import kotlinx.serialization.Serializable
@Serializable
data class ChangePasswordRequestData(val oldPassword: String, val newPassword: String)

View File

@@ -1,6 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.auth
import kotlinx.serialization.Serializable
@Serializable
data class LoginRequestData(val username: String, val password: String)

View File

@@ -1,6 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.auth
import kotlinx.serialization.Serializable
@Serializable
data class LoginResponseData(val id: String, val accessToken: String)

View File

@@ -1,12 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.auth
import kotlinx.serialization.Serializable
import ru.n08i40k.polytechnic.next.model.UserRole
@Serializable
data class RegisterRequestData(
val username: String,
val password: String,
val group: String,
val role: UserRole
)

View File

@@ -1,6 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.auth
import kotlinx.serialization.Serializable
@Serializable
data class RegisterResponseData(val id: String, val accessToken: String)

View File

@@ -1,6 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.profile
import kotlinx.serialization.Serializable
@Serializable
data class ChangeGroupRequestData(val group: String)

View File

@@ -1,6 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.profile
import kotlinx.serialization.Serializable
@Serializable
data class ChangeUsernameRequestData(val username: String)

View File

@@ -1,18 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.schedule
import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
class ScheduleGetCacheStatusReq(
context: Context,
listener: Response.Listener<ScheduleGetCacheStatusResData>,
errorListener: Response.ErrorListener? = null
) : AuthorizedRequest(
context,
Method.GET,
"schedule/cache-status",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
)

View File

@@ -1,11 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.schedule
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleGetCacheStatusResData(
val cacheUpdateRequired: Boolean,
val cacheHash: String,
val lastCacheUpdate: Long,
val lastScheduleUpdate: Long,
)

View File

@@ -1,18 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.schedule
import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.CachedRequest
class ScheduleGetGroupNamesReq(
context: Context,
listener: Response.Listener<ScheduleGetGroupNamesResData>,
errorListener: Response.ErrorListener? = null
) : CachedRequest(
context,
Method.GET,
"schedule/get-group-names",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
)

View File

@@ -1,8 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.schedule
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleGetGroupNamesResData(
val names: ArrayList<String>,
)

View File

@@ -1,24 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.schedule
import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.CachedRequest
class ScheduleGetReq(
private val data: ScheduleGetReqData,
context: Context,
listener: Response.Listener<ScheduleGetResData>,
errorListener: Response.ErrorListener? = null
) : CachedRequest(
context,
Method.POST,
"schedule/get-group",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray()
}
}

View File

@@ -1,6 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.schedule
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleGetReqData(val name: String)

View File

@@ -1,11 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.schedule
import kotlinx.serialization.Serializable
import ru.n08i40k.polytechnic.next.model.Group
@Serializable
data class ScheduleGetResData(
val updatedAt: String,
val group: Group,
val lastChangedDays: ArrayList<Int>,
)

View File

@@ -1,6 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.schedule
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleUpdateReqData(val mainPage: String)

View File

@@ -1,18 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer
import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
class ScheduleReplacerClearReq(
context: Context,
listener: Response.Listener<ScheduleReplacerClearResData>,
errorListener: Response.ErrorListener?
) : AuthorizedRequest(
context,
Method.POST,
"schedule-replacer/clear",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
)

View File

@@ -1,8 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleReplacerClearResData(
val count: Int
)

View File

@@ -1,5 +0,0 @@
package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
typealias ScheduleReplacerGetResData = List<ScheduleReplacer>

View File

@@ -1,4 +1,4 @@
package ru.n08i40k.polytechnic.next.network.data package ru.n08i40k.polytechnic.next.network.request
import android.content.Context import android.content.Context
import com.android.volley.AuthFailureError import com.android.volley.AuthFailureError
@@ -40,7 +40,7 @@ open class AuthorizedRequest(
context.settingsDataStore.data.map { settings -> settings.accessToken }.first() context.settingsDataStore.data.map { settings -> settings.accessToken }.first()
} }
if (accessToken.isEmpty()) if (accessToken.isEmpty() && context.profileViewModel != null)
context.profileViewModel!!.onUnauthorized() context.profileViewModel!!.onUnauthorized()
val headers = super.getHeaders() val headers = super.getHeaders()

View File

@@ -1,4 +1,4 @@
package ru.n08i40k.polytechnic.next.network.data package ru.n08i40k.polytechnic.next.network.request
import android.content.Context import android.content.Context
import com.android.volley.Response import com.android.volley.Response
@@ -11,10 +11,8 @@ import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.data.AppContainer import ru.n08i40k.polytechnic.next.data.AppContainer
import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.network.NetworkConnection import ru.n08i40k.polytechnic.next.network.NetworkConnection
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusReq import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetCacheStatus
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusResData import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleUpdate
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateReq
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateReqData
import ru.n08i40k.polytechnic.next.network.tryFuture import ru.n08i40k.polytechnic.next.network.tryFuture
import ru.n08i40k.polytechnic.next.network.tryGet import ru.n08i40k.polytechnic.next.network.tryGet
import java.util.logging.Logger import java.util.logging.Logger
@@ -58,14 +56,14 @@ open class CachedRequest(
} }
} }
private suspend fun updateMainPage(): MyResult<ScheduleGetCacheStatusResData> { private suspend fun updateMainPage(): MyResult<ScheduleGetCacheStatus.ResponseDto> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
when (val mainPage = getMainPage()) { when (val mainPage = getMainPage()) {
is MyResult.Failure -> mainPage is MyResult.Failure -> mainPage
is MyResult.Success -> { is MyResult.Success -> {
tryFuture { tryFuture {
ScheduleUpdateReq( ScheduleUpdate(
ScheduleUpdateReqData(mainPage.data), ScheduleUpdate.RequestDto(mainPage.data),
context, context,
it, it,
it it
@@ -83,7 +81,7 @@ open class CachedRequest(
logger.info("Getting cache status...") logger.info("Getting cache status...")
val cacheStatusResult = tryFuture { val cacheStatusResult = tryFuture {
ScheduleGetCacheStatusReq(context, it, it) ScheduleGetCacheStatus(context, it, it)
} }
if (cacheStatusResult is MyResult.Success) { if (cacheStatusResult is MyResult.Success) {

View File

@@ -1,13 +1,14 @@
package ru.n08i40k.polytechnic.next.network.data.auth package ru.n08i40k.polytechnic.next.network.request.auth
import android.content.Context import android.content.Context
import com.android.volley.Response import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class ChangePasswordRequest( class AuthChangePassword(
private val data: ChangePasswordRequestData, private val data: RequestDto,
context: Context, context: Context,
listener: Response.Listener<Nothing>, listener: Response.Listener<Nothing>,
errorListener: Response.ErrorListener? errorListener: Response.ErrorListener?
@@ -15,10 +16,13 @@ class ChangePasswordRequest(
context, context,
Method.POST, Method.POST,
"auth/change-password", "auth/change-password",
Response.Listener<String> { listener.onResponse(null) }, { listener.onResponse(null) },
errorListener, errorListener,
canBeUnauthorized = true canBeUnauthorized = true
) { ) {
@Serializable
data class RequestDto(val oldPassword: String, val newPassword: String)
override fun getBody(): ByteArray { override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray() return Json.encodeToString(data).toByteArray()
} }

View File

@@ -1,15 +1,16 @@
package ru.n08i40k.polytechnic.next.network.data.auth package ru.n08i40k.polytechnic.next.network.request.auth
import android.content.Context import android.content.Context
import com.android.volley.Response import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.RequestBase import ru.n08i40k.polytechnic.next.network.RequestBase
class LoginRequest( class AuthLogin(
private val data: LoginRequestData, private val data: RequestDto,
context: Context, context: Context,
listener: Response.Listener<LoginResponseData>, listener: Response.Listener<ResponseDto>,
errorListener: Response.ErrorListener? errorListener: Response.ErrorListener?
) : RequestBase( ) : RequestBase(
context, context,
@@ -18,6 +19,12 @@ class LoginRequest(
{ listener.onResponse(Json.decodeFromString(it)) }, { listener.onResponse(Json.decodeFromString(it)) },
errorListener errorListener
) { ) {
@Serializable
data class RequestDto(val username: String, val password: String)
@Serializable
data class ResponseDto(val id: String, val accessToken: String)
override fun getBody(): ByteArray { override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray() return Json.encodeToString(data).toByteArray()
} }

View File

@@ -1,15 +1,17 @@
package ru.n08i40k.polytechnic.next.network.data.auth package ru.n08i40k.polytechnic.next.network.request.auth
import android.content.Context import android.content.Context
import com.android.volley.Response import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.model.UserRole
import ru.n08i40k.polytechnic.next.network.RequestBase import ru.n08i40k.polytechnic.next.network.RequestBase
class RegisterRequest( class AuthRegister(
private val data: RegisterRequestData, private val data: RequestDto,
context: Context, context: Context,
listener: Response.Listener<RegisterResponseData>, listener: Response.Listener<ResponseDto>,
errorListener: Response.ErrorListener? errorListener: Response.ErrorListener?
) : RequestBase( ) : RequestBase(
context, context,
@@ -18,6 +20,17 @@ class RegisterRequest(
{ listener.onResponse(Json.decodeFromString(it)) }, { listener.onResponse(Json.decodeFromString(it)) },
errorListener errorListener
) { ) {
@Serializable
data class RequestDto(
val username: String,
val password: String,
val group: String,
val role: UserRole
)
@Serializable
data class ResponseDto(val id: String, val accessToken: String)
override fun getBody(): ByteArray { override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray() return Json.encodeToString(data).toByteArray()
} }

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 FcmSetToken(
context: Context,
token: String,
listener: Response.Listener<Unit>,
errorListener: Response.ErrorListener?,
) : AuthorizedRequest(
context, Method.POST,
"fcm/set-token/$token",
{ listener.onResponse(Unit) },
errorListener,
true
)

View File

@@ -1,13 +1,14 @@
package ru.n08i40k.polytechnic.next.network.data.profile package ru.n08i40k.polytechnic.next.network.request.profile
import android.content.Context import android.content.Context
import com.android.volley.Response import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class ChangeGroupRequest( class ProfileChangeGroup(
private val data: ChangeGroupRequestData, private val data: RequestDto,
context: Context, context: Context,
listener: Response.Listener<Nothing>, listener: Response.Listener<Nothing>,
errorListener: Response.ErrorListener? errorListener: Response.ErrorListener?
@@ -15,9 +16,12 @@ class ChangeGroupRequest(
context, context,
Method.POST, Method.POST,
"users/change-group", "users/change-group",
Response.Listener<String> { listener.onResponse(null) }, { listener.onResponse(null) },
errorListener errorListener
) { ) {
@Serializable
data class RequestDto(val group: String)
override fun getBody(): ByteArray { override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray() return Json.encodeToString(data).toByteArray()
} }

View File

@@ -1,13 +1,14 @@
package ru.n08i40k.polytechnic.next.network.data.profile package ru.n08i40k.polytechnic.next.network.request.profile
import android.content.Context import android.content.Context
import com.android.volley.Response import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class ChangeUsernameRequest( class ProfileChangeUsername(
private val data: ChangeUsernameRequestData, private val data: RequestDto,
context: Context, context: Context,
listener: Response.Listener<Nothing>, listener: Response.Listener<Nothing>,
errorListener: Response.ErrorListener? errorListener: Response.ErrorListener?
@@ -15,9 +16,12 @@ class ChangeUsernameRequest(
context, context,
Method.POST, Method.POST,
"users/change-username", "users/change-username",
Response.Listener<String> { listener.onResponse(null) }, { listener.onResponse(null) },
errorListener errorListener
) { ) {
@Serializable
data class RequestDto(val username: String)
override fun getBody(): ByteArray { override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray() return Json.encodeToString(data).toByteArray()
} }

View File

@@ -1,12 +1,12 @@
package ru.n08i40k.polytechnic.next.network.data.profile package ru.n08i40k.polytechnic.next.network.request.profile
import android.content.Context import android.content.Context
import com.android.volley.Response import com.android.volley.Response
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.model.Profile import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class UsersMeRequest( class ProfileMe(
context: Context, context: Context,
listener: Response.Listener<Profile>, listener: Response.Listener<Profile>,
errorListener: Response.ErrorListener? errorListener: Response.ErrorListener?

View File

@@ -0,0 +1,36 @@
package ru.n08i40k.polytechnic.next.network.request.schedule
import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
class ScheduleGet(
private val data: RequestDto,
context: Context,
listener: Response.Listener<ResponseDto>,
errorListener: Response.ErrorListener? = null
) : CachedRequest(
context,
Method.POST,
"schedule/get-group",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
@Serializable
data class RequestDto(val name: String)
@Serializable
data class ResponseDto(
val updatedAt: String,
val group: Group,
val lastChangedDays: ArrayList<Int>,
)
override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray()
}
}

View File

@@ -0,0 +1,27 @@
package ru.n08i40k.polytechnic.next.network.request.schedule
import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class ScheduleGetCacheStatus(
context: Context,
listener: Response.Listener<ResponseDto>,
errorListener: Response.ErrorListener? = null
) : AuthorizedRequest(
context,
Method.GET,
"schedule/cache-status",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
@Serializable
data class ResponseDto(
val cacheUpdateRequired: Boolean,
val cacheHash: String,
val lastCacheUpdate: Long,
val lastScheduleUpdate: Long,
)
}

View File

@@ -0,0 +1,24 @@
package ru.n08i40k.polytechnic.next.network.request.schedule
import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
class ScheduleGetGroupNames(
context: Context,
listener: Response.Listener<ResponseDto>,
errorListener: Response.ErrorListener? = null
) : CachedRequest(
context,
Method.GET,
"schedule/get-group-names",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
@Serializable
data class ResponseDto(
val names: ArrayList<String>,
)
}

View File

@@ -1,15 +1,16 @@
package ru.n08i40k.polytechnic.next.network.data.schedule package ru.n08i40k.polytechnic.next.network.request.schedule
import android.content.Context import android.content.Context
import com.android.volley.Response import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class ScheduleUpdateReq( class ScheduleUpdate(
private val data: ScheduleUpdateReqData, private val data: RequestDto,
context: Context, context: Context,
listener: Response.Listener<ScheduleGetCacheStatusResData>, listener: Response.Listener<ScheduleGetCacheStatus.ResponseDto>,
errorListener: Response.ErrorListener? = null errorListener: Response.ErrorListener? = null
) : AuthorizedRequest( ) : AuthorizedRequest(
context, context,
@@ -18,6 +19,9 @@ class ScheduleUpdateReq(
{ listener.onResponse(Json.decodeFromString(it)) }, { listener.onResponse(Json.decodeFromString(it)) },
errorListener errorListener
) { ) {
@Serializable
data class RequestDto(val mainPage: String)
override fun getBody(): ByteArray { override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray() return Json.encodeToString(data).toByteArray()
} }

View File

@@ -0,0 +1,24 @@
package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class ScheduleReplacerClear(
context: Context,
listener: Response.Listener<ResponseDto>,
errorListener: Response.ErrorListener?
) : AuthorizedRequest(
context,
Method.POST,
"schedule-replacer/clear",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
@Serializable
data class ResponseDto(
val count: Int
)
}

View File

@@ -1,13 +1,14 @@
package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
import android.content.Context import android.content.Context
import com.android.volley.Response import com.android.volley.Response
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class ScheduleReplacerGetReq( class ScheduleReplacerGet(
context: Context, context: Context,
listener: Response.Listener<ScheduleReplacerGetResData>, listener: Response.Listener<List<ScheduleReplacer>>,
errorListener: Response.ErrorListener? errorListener: Response.ErrorListener?
) : AuthorizedRequest( ) : AuthorizedRequest(
context, context,

View File

@@ -1,10 +1,10 @@
package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
import android.content.Context import android.content.Context
import com.android.volley.Response import com.android.volley.Response
import ru.n08i40k.polytechnic.next.network.AuthorizedMultipartRequest import ru.n08i40k.polytechnic.next.network.AuthorizedMultipartRequest
class ScheduleReplacerSetReq( class ScheduleReplacerSet(
context: Context, context: Context,
private val fileName: String, private val fileName: String,
private val fileData: ByteArray, private val fileData: ByteArray,

View File

@@ -0,0 +1,110 @@
package ru.n08i40k.polytechnic.next.service
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.NotificationChannels
import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
import java.time.Duration
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val request = OneTimeWorkRequestBuilder<SetFcmTokenWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, Duration.ofMinutes(1))
.setInputData(workDataOf("TOKEN" to token))
.build()
WorkManager
.getInstance(applicationContext)
.enqueue(request)
}
override fun onMessageReceived(message: RemoteMessage) {
val type = message.data["type"]
when (type) {
"schedule-update" -> {
val notification = NotificationCompat
.Builder(applicationContext, NotificationChannels.SCHEDULE_UPDATE)
.setSmallIcon(R.drawable.logo)
.setContentTitle(getString(R.string.schedule_update_title))
.setContentText(
getString(
if (message.data["replaced"] == "true")
R.string.schedule_update_replaced
else
R.string.schedule_update_default
)
)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
.build()
with(NotificationManagerCompat.from(this)) {
if (ActivityCompat.checkSelfPermission(
this@MyFirebaseMessagingService,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return@with
}
notify(message.data["etag"].hashCode(), notification)
}
}
}
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,9 +1,17 @@
package ru.n08i40k.polytechnic.next.ui package ru.n08i40k.polytechnic.next.ui
import android.Manifest
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
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
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
@@ -11,20 +19,95 @@ import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeContent import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.PeriodicWorkRequest
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.NotificationChannels.SCHEDULE_UPDATE
import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.settings.settingsDataStore import ru.n08i40k.polytechnic.next.settings.settingsDataStore
import java.util.concurrent.TimeUnit
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@SuppressLint("ObsoleteSdkInt")
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = getString(R.string.schedule_channel_name)
val description = getString(R.string.schedule_channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(SCHEDULE_UPDATE, name, importance)
channel.description = description
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) {
if (it) {
createNotificationChannel()
}
}
private fun hasNotificationPermission(): Boolean {
return (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|| ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED)
}
private fun askNotificationPermission() {
if (!hasNotificationPermission())
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() {
val workRequest = PeriodicWorkRequest.Builder(
CacheUpdateWorker::class.java,
15, TimeUnit.MINUTES
)
.addTag("schedule-update")
.build()
val workManager = WorkManager.getInstance(applicationContext)
workManager.cancelAllWorkByTag("schedule-update")
workManager.enqueue(workRequest)
}
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()
if (hasNotificationPermission())
createNotificationChannel()
setContent { setContent {
Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) { Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) {
PolytechnicApp() PolytechnicApp()

View File

@@ -59,11 +59,9 @@ import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.R 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.model.UserRole.Companion.AcceptableUserRoles import ru.n08i40k.polytechnic.next.model.UserRole.Companion.AcceptableUserRoles
import ru.n08i40k.polytechnic.next.network.data.auth.LoginRequest import ru.n08i40k.polytechnic.next.network.request.auth.AuthLogin
import ru.n08i40k.polytechnic.next.network.data.auth.LoginRequestData import ru.n08i40k.polytechnic.next.network.request.auth.AuthRegister
import ru.n08i40k.polytechnic.next.network.data.auth.RegisterRequest import ru.n08i40k.polytechnic.next.network.request.profile.ProfileMe
import ru.n08i40k.polytechnic.next.network.data.auth.RegisterRequestData
import ru.n08i40k.polytechnic.next.network.data.profile.UsersMeRequest
import ru.n08i40k.polytechnic.next.settings.settingsDataStore import ru.n08i40k.polytechnic.next.settings.settingsDataStore
@Preview(showBackground = true) @Preview(showBackground = true)
@@ -368,7 +366,7 @@ fun tryLogin(
isLoading = true isLoading = true
LoginRequest(LoginRequestData(username, password), context, { AuthLogin(AuthLogin.RequestDto(username, password), context, {
runBlocking { runBlocking {
context.settingsDataStore.updateData { currentSettings -> context.settingsDataStore.updateData { currentSettings ->
currentSettings currentSettings
@@ -379,7 +377,7 @@ fun tryLogin(
} }
} }
UsersMeRequest(context, { ProfileMe(context, {
scope.launch { snackbarHostState.showSnackbar("Cool!") } scope.launch { snackbarHostState.showSnackbar("Cool!") }
runBlocking { runBlocking {
@@ -437,8 +435,8 @@ fun tryRegister(
isLoading = true isLoading = true
RegisterRequest( AuthRegister(
RegisterRequestData( AuthRegister.RequestDto(
username, username,
password, password,
group, group,

View File

@@ -35,9 +35,8 @@ import com.android.volley.ClientError
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
import ru.n08i40k.polytechnic.next.model.Profile import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.network.data.profile.ChangeGroupRequest import ru.n08i40k.polytechnic.next.network.request.profile.ProfileChangeGroup
import ru.n08i40k.polytechnic.next.network.data.profile.ChangeGroupRequestData import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetGroupNames
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetGroupNamesReq
private enum class ChangeGroupError { private enum class ChangeGroupError {
NOT_EXISTS NOT_EXISTS
@@ -49,7 +48,7 @@ private fun tryChangeGroup(
onError: (ChangeGroupError) -> Unit, onError: (ChangeGroupError) -> Unit,
onSuccess: (String) -> Unit onSuccess: (String) -> Unit
) { ) {
ChangeGroupRequest(ChangeGroupRequestData(group), context, { ProfileChangeGroup(ProfileChangeGroup.RequestDto(group), context, {
onSuccess(group) onSuccess(group)
}, { }, {
if (it is ClientError && it.networkResponse.statusCode == 404) if (it is ClientError && it.networkResponse.statusCode == 404)
@@ -65,7 +64,7 @@ private fun getGroups(context: Context): ArrayList<String> {
val groups = remember { arrayListOf(groupPlaceholder) } val groups = remember { arrayListOf(groupPlaceholder) }
LaunchedEffect(groups) { LaunchedEffect(groups) {
ScheduleGetGroupNamesReq(context, { ScheduleGetGroupNames(context, {
groups.clear() groups.clear()
groups.addAll(it.names) groups.addAll(it.names)
}, { }, {

View File

@@ -26,8 +26,7 @@ import com.android.volley.ClientError
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
import ru.n08i40k.polytechnic.next.model.Profile import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.network.data.auth.ChangePasswordRequest import ru.n08i40k.polytechnic.next.network.request.auth.AuthChangePassword
import ru.n08i40k.polytechnic.next.network.data.auth.ChangePasswordRequestData
private enum class ChangePasswordError { private enum class ChangePasswordError {
INCORRECT_CURRENT_PASSWORD, INCORRECT_CURRENT_PASSWORD,
@@ -41,7 +40,7 @@ private fun tryChangePassword(
onError: (ChangePasswordError) -> Unit, onError: (ChangePasswordError) -> Unit,
onSuccess: () -> Unit onSuccess: () -> Unit
) { ) {
ChangePasswordRequest(ChangePasswordRequestData(oldPassword, newPassword), context, { AuthChangePassword(AuthChangePassword.RequestDto(oldPassword, newPassword), context, {
onSuccess() onSuccess()
}, { }, {
if (it is ClientError && it.networkResponse.statusCode == 409) if (it is ClientError && it.networkResponse.statusCode == 409)

View File

@@ -24,8 +24,7 @@ import com.android.volley.ClientError
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
import ru.n08i40k.polytechnic.next.model.Profile import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.network.data.profile.ChangeUsernameRequest import ru.n08i40k.polytechnic.next.network.request.profile.ProfileChangeUsername
import ru.n08i40k.polytechnic.next.network.data.profile.ChangeUsernameRequestData
private enum class ChangeUsernameError { private enum class ChangeUsernameError {
INCORRECT_LENGTH, INCORRECT_LENGTH,
@@ -38,7 +37,7 @@ private fun tryChangeUsername(
onError: (ChangeUsernameError) -> Unit, onError: (ChangeUsernameError) -> Unit,
onSuccess: () -> Unit onSuccess: () -> Unit
) { ) {
ChangeUsernameRequest(ChangeUsernameRequestData(username), context, { ProfileChangeUsername(ProfileChangeUsername.RequestDto(username), context, {
onSuccess() onSuccess()
}, { }, {
if (it is ClientError && it.networkResponse.statusCode == 409) if (it is ClientError && it.networkResponse.statusCode == 409)

View File

@@ -41,7 +41,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MockAppContainer import ru.n08i40k.polytechnic.next.data.MockAppContainer
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleReplacerRepository import ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl.FakeScheduleReplacerRepository
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
import ru.n08i40k.polytechnic.next.ui.LoadingContent import ru.n08i40k.polytechnic.next.ui.LoadingContent
import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerUiState import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerUiState

View File

@@ -1,7 +1,5 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule package ru.n08i40k.polytechnic.next.ui.main.schedule
import android.os.Handler
import android.os.Looper
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
@@ -17,12 +15,10 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -30,6 +26,10 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
import ru.n08i40k.polytechnic.next.model.Day import ru.n08i40k.polytechnic.next.model.Day
@@ -44,26 +44,39 @@ private fun getCurrentMinutes(): Int {
} }
@Composable @Composable
private fun getMinutes(): Int { private fun getMinutes(): Flow<Int> {
var value by remember { mutableIntStateOf(getCurrentMinutes()) } val value by remember {
derivedStateOf {
DisposableEffect(Unit) { flow {
val handler = Handler(Looper.getMainLooper()) while (true) {
emit(getCurrentMinutes())
val runnable = { delay(5_000)
value = getCurrentMinutes() }
} }
handler.postDelayed(runnable, 60_000)
onDispose {
handler.removeCallbacks(runnable)
} }
} }
return value return value
} }
@Composable
fun calculateCurrentLessonIdx(lessons: ArrayList<Lesson?>): Int {
val currentMinutes by getMinutes().collectAsStateWithLifecycle(0)
val filteredLessons = lessons
.filterNotNull()
.filter {
it.time != null
&& it.time.start >= currentMinutes
&& it.time.end <= currentMinutes
}
if (filteredLessons.isEmpty())
return -1
return lessons.indexOf(filteredLessons[0])
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun DayCard( fun DayCard(
@@ -71,9 +84,26 @@ fun DayCard(
day: Day? = FakeScheduleRepository.exampleGroup.days[0], day: Day? = FakeScheduleRepository.exampleGroup.days[0],
current: Boolean = true current: Boolean = true
) { ) {
val defaultCardColors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
)
val customCardColors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
)
val noneCardColors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
)
Card( Card(
modifier = modifier, modifier = modifier,
colors = CardDefaults.cardColors(containerColor = if (current) MaterialTheme.colorScheme.surfaceContainerHighest else MaterialTheme.colorScheme.surfaceContainerLowest) colors = CardDefaults.cardColors(
containerColor =
if (current) MaterialTheme.colorScheme.surfaceContainerHighest
else MaterialTheme.colorScheme.surfaceContainerLowest
)
) { ) {
if (day == null) { if (day == null) {
Text( Text(
@@ -92,34 +122,16 @@ fun DayCard(
text = day.name, text = day.name,
) )
val currentMinutes = getMinutes() val currentLessonIdx = calculateCurrentLessonIdx(day.lessons)
val isCurrentLesson: (lesson: Lesson) -> Boolean = {
current
&& it.time != null
&& currentMinutes >= it.time.start
&& currentMinutes <= it.time.end
}
Column( Column(
modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(0.5.dp) modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(0.5.dp)
) { ) {
if (day.nonNullIndices.isEmpty()) { if (day.nonNullIndices.isEmpty()) {
Text("Can't get schedule!") Text("Can't get schedule!")
} else { return@Column
val defaultCardColors = CardDefaults.cardColors( }
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
val customCardColors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
)
val noneCardColors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
)
for (i in day.nonNullIndices.first()..day.nonNullIndices.last()) { for (i in day.nonNullIndices.first()..day.nonNullIndices.last()) {
val lesson = day.lessons[i]!! val lesson = day.lessons[i]!!
@@ -139,7 +151,8 @@ fun DayCard(
} }
Box( Box(
modifier = if (isCurrentLesson(lesson)) lessonBoxModifier.border( modifier =
if (i == currentLessonIdx) lessonBoxModifier.border(
border = BorderStroke( border = BorderStroke(
3.5.dp, 3.5.dp,
Color( Color(
@@ -149,7 +162,8 @@ fun DayCard(
1F 1F
) )
) )
) else lessonBoxModifier )
else lessonBoxModifier
) { ) {
LessonRow( LessonRow(
day, lesson, cardColors day, lesson, cardColors
@@ -169,10 +183,8 @@ fun DayCard(
} }
} }
if (mutableExpanded.value) LessonExtraInfo( if (mutableExpanded.value)
lesson, mutableExpanded LessonExtraInfo(lesson, mutableExpanded)
)
}
} }
} }
} }

View File

@@ -6,26 +6,63 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.delay
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MockAppContainer import ru.n08i40k.polytechnic.next.data.MockAppContainer
import ru.n08i40k.polytechnic.next.ui.LoadingContent import ru.n08i40k.polytechnic.next.ui.LoadingContent
import ru.n08i40k.polytechnic.next.ui.model.ScheduleUiState import ru.n08i40k.polytechnic.next.ui.model.ScheduleUiState
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
@Composable
private fun rememberUpdatedLifecycleOwner(): LifecycleOwner {
val lifecycleOwner = LocalLifecycleOwner.current
return remember { lifecycleOwner }
}
@Preview(showBackground = true, showSystemUi = true) @Preview(showBackground = true, showSystemUi = true)
@Composable @Composable
fun ScheduleScreen( fun ScheduleScreen(
scheduleViewModel: ScheduleViewModel = ScheduleViewModel(MockAppContainer(LocalContext.current)), scheduleViewModel: ScheduleViewModel = ScheduleViewModel(MockAppContainer(LocalContext.current)),
onRefreshSchedule: () -> Unit = {} onRefresh: () -> Unit = {}
) { ) {
val uiState by scheduleViewModel.uiState.collectAsStateWithLifecycle() val uiState by scheduleViewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(uiState) {
delay(120_000)
onRefresh()
}
val lifecycleOwner = rememberUpdatedLifecycleOwner()
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
onRefresh()
}
else -> Unit
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
LoadingContent( LoadingContent(
empty = when (uiState) { empty = when (uiState) {
@@ -33,13 +70,14 @@ fun ScheduleScreen(
is ScheduleUiState.HasSchedule -> false is ScheduleUiState.HasSchedule -> false
}, },
loading = uiState.isLoading, loading = uiState.isLoading,
onRefresh = { onRefreshSchedule() }, onRefresh = onRefresh,
verticalArrangement = Arrangement.Top verticalArrangement = Arrangement.Top
) { ) {
when (uiState) { when (uiState) {
is ScheduleUiState.HasSchedule -> { is ScheduleUiState.HasSchedule -> {
Column { Column {
val hasSchedule = uiState as ScheduleUiState.HasSchedule val hasSchedule = uiState as ScheduleUiState.HasSchedule
UpdateInfo(hasSchedule.lastUpdateAt, hasSchedule.updateDates) UpdateInfo(hasSchedule.lastUpdateAt, hasSchedule.updateDates)
DayPager(hasSchedule.group) DayPager(hasSchedule.group)
} }
@@ -47,7 +85,7 @@ fun ScheduleScreen(
is ScheduleUiState.NoSchedule -> { is ScheduleUiState.NoSchedule -> {
if (!uiState.isLoading) { if (!uiState.isLoading) {
TextButton(onClick = onRefreshSchedule, modifier = Modifier.fillMaxSize()) { TextButton(onClick = onRefresh, modifier = Modifier.fillMaxSize()) {
Text(stringResource(R.string.reload), textAlign = TextAlign.Center) Text(stringResource(R.string.reload), textAlign = TextAlign.Center)
} }
} }

View File

@@ -40,4 +40,9 @@
<string name="bytes">байт</string> <string name="bytes">байт</string>
<string name="clear_replacers">Удалить всё</string> <string name="clear_replacers">Удалить всё</string>
<string name="set_replacer">Загрузить новое расписание</string> <string name="set_replacer">Загрузить новое расписание</string>
<string name="schedule_channel_name">Обновления расписания</string>
<string name="schedule_channel_description">Информирует об обновлении расписания</string>
<string name="schedule_update_title">Расписание обновлено.</string>
<string name="schedule_update_replaced">Расписание было обновлено Администратором.</string>
<string name="schedule_update_default">Расписание было обновлено на сайте политехникума.</string>
</resources> </resources>

View File

@@ -40,4 +40,9 @@
<string name="bytes">bytes</string> <string name="bytes">bytes</string>
<string name="clear_replacers">Clear</string> <string name="clear_replacers">Clear</string>
<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_description">Inform when 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_default">Schedule was updated on polytechnic website.</string>
</resources> </resources>

View File

@@ -1,7 +1,7 @@
[versions] [versions]
accompanistSwiperefresh = "0.36.0" accompanistSwiperefresh = "0.36.0"
agp = "8.6.1" agp = "8.6.1"
firebaseBom = "33.3.0" firebaseBom = "33.4.0"
hiltAndroid = "2.52" hiltAndroid = "2.52"
hiltAndroidCompiler = "2.52" hiltAndroidCompiler = "2.52"
hiltNavigationCompose = "1.2.0" hiltNavigationCompose = "1.2.0"
@@ -13,18 +13,22 @@ espressoCore = "3.6.1"
kotlinxSerializationJson = "1.7.3" kotlinxSerializationJson = "1.7.3"
lifecycleRuntimeKtx = "2.8.6" lifecycleRuntimeKtx = "2.8.6"
activityCompose = "1.9.2" activityCompose = "1.9.2"
composeBom = "2024.09.02" composeBom = "2024.09.03"
protobufLite = "3.0.1" protobufLite = "3.0.1"
volley = "1.2.1" volley = "1.2.1"
datastore = "1.1.1" datastore = "1.1.1"
navigationCompose = "2.8.1" navigationCompose = "2.8.2"
firebaseCrashlytics = "19.1.0" firebaseCrashlytics = "19.2.0"
googleFirebaseCrashlytics = "3.0.2" googleFirebaseCrashlytics = "3.0.2"
firebaseMessaging = "24.0.2"
workRuntime = "2.9.1"
[libraries] [libraries]
accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanistSwiperefresh" } accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanistSwiperefresh" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
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-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntime" }
firebase-analytics = { module = "com.google.firebase:firebase-analytics" } firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } 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" }
@@ -40,7 +44,7 @@ androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version = "1.7.2" } androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version = "1.7.3" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
@@ -48,6 +52,7 @@ protobuf-lite = { module = "com.google.protobuf:protobuf-lite", version.ref = "p
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-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics", version.ref = "firebaseCrashlytics" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging", version.ref = "firebaseMessaging" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }