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:
@@ -32,8 +32,8 @@ android {
|
||||
applicationId = "ru.n08i40k.polytechnic.next"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 10
|
||||
versionName = "1.5.0"
|
||||
versionCode = 11
|
||||
versionName = "1.6.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -103,6 +103,7 @@ dependencies {
|
||||
implementation(libs.firebase.analytics)
|
||||
implementation(libs.firebase.messaging)
|
||||
implementation(libs.firebase.crashlytics)
|
||||
implementation(libs.firebase.config)
|
||||
|
||||
// datastore
|
||||
implementation(libs.androidx.datastore)
|
||||
|
||||
@@ -2,4 +2,5 @@ package ru.n08i40k.polytechnic.next
|
||||
|
||||
object NotificationChannels {
|
||||
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 dagger.hilt.android.HiltAndroidApp
|
||||
import ru.n08i40k.polytechnic.next.data.AppContainer
|
||||
import ru.n08i40k.polytechnic.next.utils.or
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
@@ -10,4 +11,10 @@ class PolytechnicApplication : Application() {
|
||||
@Suppress("unused")
|
||||
@Inject
|
||||
lateinit var container: AppContainer
|
||||
|
||||
fun getAppVersion(): String {
|
||||
return applicationContext.packageManager
|
||||
.getPackageInfo(this.packageName, 0)
|
||||
.versionName or "1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,8 @@ open class AuthorizedRequest(
|
||||
.setAccessToken("").build()
|
||||
}
|
||||
}
|
||||
context.profileViewModel!!.onUnauthorized()
|
||||
if (context.profileViewModel != null)
|
||||
context.profileViewModel!!.onUnauthorized()
|
||||
}
|
||||
|
||||
errorListener?.onErrorResponse(it)
|
||||
|
||||
@@ -78,8 +78,6 @@ open class CachedRequest(
|
||||
val logger = Logger.getLogger("CachedRequest")
|
||||
val repository = appContainer.networkCacheRepository
|
||||
|
||||
logger.info("Getting cache status...")
|
||||
|
||||
val cacheStatusResult = tryFuture {
|
||||
ScheduleGetCacheStatus(context, it, it)
|
||||
}
|
||||
@@ -87,8 +85,6 @@ open class CachedRequest(
|
||||
if (cacheStatusResult is MyResult.Success) {
|
||||
val cacheStatus = cacheStatusResult.data
|
||||
|
||||
logger.info("Cache status received successfully!")
|
||||
|
||||
runBlocking {
|
||||
repository.setUpdateDates(
|
||||
cacheStatus.lastCacheUpdate,
|
||||
@@ -98,12 +94,10 @@ open class CachedRequest(
|
||||
}
|
||||
|
||||
if (cacheStatus.cacheUpdateRequired) {
|
||||
logger.info("Cache update was required!")
|
||||
val updateResult = runBlocking { updateMainPage() }
|
||||
|
||||
when (updateResult) {
|
||||
is MyResult.Success -> {
|
||||
logger.info("Cache update was successful!")
|
||||
runBlocking {
|
||||
repository.setUpdateDates(
|
||||
updateResult.data.lastCacheUpdate,
|
||||
@@ -124,12 +118,10 @@ open class CachedRequest(
|
||||
|
||||
val cachedResponse = runBlocking { repository.get(url) }
|
||||
if (cachedResponse != null) {
|
||||
logger.info("Found cached response!")
|
||||
listener.onResponse(cachedResponse.data)
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("Cached response doesn't exists!")
|
||||
super.send()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
@@ -11,19 +14,12 @@ import androidx.work.Constraints
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.n08i40k.polytechnic.next.NotificationChannels
|
||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||
import ru.n08i40k.polytechnic.next.work.FcmSetTokenWorker
|
||||
import java.time.Duration
|
||||
|
||||
class MyFirebaseMessagingService : FirebaseMessagingService() {
|
||||
@@ -34,7 +30,7 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
val request = OneTimeWorkRequestBuilder<SetFcmTokenWorker>()
|
||||
val request = OneTimeWorkRequestBuilder<FcmSetTokenWorker>()
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, Duration.ofMinutes(1))
|
||||
.setInputData(workDataOf("TOKEN" to token))
|
||||
@@ -45,66 +41,79 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
|
||||
.enqueue(request)
|
||||
}
|
||||
|
||||
private fun sendNotification(
|
||||
channel: String,
|
||||
@DrawableRes iconId: Int,
|
||||
title: String,
|
||||
contentText: String,
|
||||
priority: Int,
|
||||
id: Any?,
|
||||
intent: Intent? = null
|
||||
) {
|
||||
val pendingIntent: PendingIntent? =
|
||||
if (intent != null)
|
||||
PendingIntent.getActivity(this, 0, intent.apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}, PendingIntent.FLAG_IMMUTABLE)
|
||||
else
|
||||
null
|
||||
|
||||
val notification = NotificationCompat
|
||||
.Builder(applicationContext, channel)
|
||||
.setSmallIcon(iconId)
|
||||
.setContentTitle(title)
|
||||
.setContentText(contentText)
|
||||
.setPriority(priority)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
|
||||
with(NotificationManagerCompat.from(this)) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
this@MyFirebaseMessagingService,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return@with
|
||||
}
|
||||
|
||||
notify(id.hashCode(), notification)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
val type = message.data["type"]
|
||||
|
||||
when (type) {
|
||||
"schedule-update" -> {
|
||||
val notification = NotificationCompat
|
||||
.Builder(applicationContext, NotificationChannels.SCHEDULE_UPDATE)
|
||||
.setSmallIcon(R.drawable.logo)
|
||||
.setContentTitle(getString(R.string.schedule_update_title))
|
||||
.setContentText(
|
||||
getString(
|
||||
if (message.data["replaced"] == "true")
|
||||
R.string.schedule_update_replaced
|
||||
else
|
||||
R.string.schedule_update_default
|
||||
)
|
||||
)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
sendNotification(
|
||||
NotificationChannels.SCHEDULE_UPDATE,
|
||||
R.drawable.logo,
|
||||
getString(R.string.schedule_update_title),
|
||||
getString(
|
||||
if (message.data["replaced"] == "true")
|
||||
R.string.schedule_update_replaced
|
||||
else
|
||||
R.string.schedule_update_default
|
||||
),
|
||||
NotificationCompat.PRIORITY_DEFAULT,
|
||||
message.data["etag"]
|
||||
)
|
||||
}
|
||||
|
||||
with(NotificationManagerCompat.from(this)) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
this@MyFirebaseMessagingService,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return@with
|
||||
}
|
||||
|
||||
notify(message.data["etag"].hashCode(), notification)
|
||||
}
|
||||
"app-update" -> {
|
||||
sendNotification(
|
||||
NotificationChannels.APP_UPDATE,
|
||||
R.drawable.logo,
|
||||
getString(R.string.app_update_title, message.data["version"]),
|
||||
getString(R.string.app_update_description),
|
||||
NotificationCompat.PRIORITY_DEFAULT,
|
||||
message.data["version"],
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse(message.data["downloadLink"]))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
super.onMessageReceived(message)
|
||||
}
|
||||
|
||||
class SetFcmTokenWorker(context: Context, workerParams: WorkerParameters) :
|
||||
Worker(context, workerParams) {
|
||||
override fun doWork(): Result {
|
||||
val fcmToken = inputData.getString("TOKEN") ?: return Result.failure()
|
||||
|
||||
val accessToken = runBlocking {
|
||||
applicationContext.settingsDataStore.data.map { it.accessToken }.first()
|
||||
}
|
||||
if (accessToken.isEmpty())
|
||||
return Result.retry()
|
||||
|
||||
val setResult = runBlocking {
|
||||
(applicationContext as PolytechnicApplication)
|
||||
.container
|
||||
.profileRepository
|
||||
.setFcmToken(fcmToken)
|
||||
}
|
||||
|
||||
return when (setResult) {
|
||||
is MyResult.Success -> Result.success()
|
||||
is MyResult.Failure -> Result.retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
package ru.n08i40k.polytechnic.next.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
@@ -22,44 +21,76 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
|
||||
import com.google.firebase.remoteconfig.remoteConfig
|
||||
import com.google.firebase.remoteconfig.remoteConfigSettings
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.n08i40k.polytechnic.next.NotificationChannels.SCHEDULE_UPDATE
|
||||
import ru.n08i40k.polytechnic.next.NotificationChannels
|
||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||
import ru.n08i40k.polytechnic.next.work.FcmUpdateCallbackWorker
|
||||
import ru.n08i40k.polytechnic.next.work.LinkUpdateWorker
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val name = getString(R.string.schedule_channel_name)
|
||||
val description = getString(R.string.schedule_channel_description)
|
||||
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
||||
val channel = NotificationChannel(SCHEDULE_UPDATE, name, importance)
|
||||
channel.description = description
|
||||
val remoteConfig: FirebaseRemoteConfig = Firebase.remoteConfig
|
||||
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
private val configSettings = remoteConfigSettings {
|
||||
minimumFetchIntervalInSeconds = 3600
|
||||
}
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission(),
|
||||
private fun createNotificationChannel(
|
||||
notificationManager: NotificationManager,
|
||||
name: String,
|
||||
description: String,
|
||||
channelId: String
|
||||
) {
|
||||
if (it) {
|
||||
createNotificationChannel()
|
||||
}
|
||||
val channel = NotificationChannel(channelId, name, NotificationManager.IMPORTANCE_DEFAULT)
|
||||
channel.description = description
|
||||
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun createNotificationChannels() {
|
||||
if (!hasNotificationPermission())
|
||||
return
|
||||
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
createNotificationChannel(
|
||||
notificationManager,
|
||||
getString(R.string.schedule_channel_name),
|
||||
getString(R.string.schedule_channel_description),
|
||||
NotificationChannels.SCHEDULE_UPDATE
|
||||
)
|
||||
|
||||
createNotificationChannel(
|
||||
notificationManager,
|
||||
getString(R.string.app_update_channel_name),
|
||||
getString(R.string.app_update_channel_description),
|
||||
NotificationChannels.APP_UPDATE
|
||||
)
|
||||
}
|
||||
|
||||
private val requestPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
if (it) createNotificationChannels()
|
||||
}
|
||||
|
||||
private fun hasNotificationPermission(): Boolean {
|
||||
return (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
|| ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
@@ -71,42 +102,72 @@ class MainActivity : ComponentActivity() {
|
||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
|
||||
class CacheUpdateWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
override fun doWork(): Result {
|
||||
runBlocking {
|
||||
(applicationContext as PolytechnicApplication)
|
||||
.container
|
||||
.scheduleRepository
|
||||
.getGroup()
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
private fun schedulePeriodicRequest() {
|
||||
fun scheduleLinkUpdate(intervalInMinutes: Long) {
|
||||
val tag = "schedule-update"
|
||||
|
||||
val workRequest = PeriodicWorkRequest.Builder(
|
||||
CacheUpdateWorker::class.java,
|
||||
15, TimeUnit.MINUTES
|
||||
LinkUpdateWorker::class.java,
|
||||
intervalInMinutes.coerceAtLeast(15), TimeUnit.MINUTES
|
||||
)
|
||||
.addTag("schedule-update")
|
||||
.addTag(tag)
|
||||
.build()
|
||||
|
||||
val workManager = WorkManager.getInstance(applicationContext)
|
||||
|
||||
workManager.cancelAllWorkByTag("schedule-update")
|
||||
workManager.cancelAllWorkByTag(tag)
|
||||
workManager.enqueue(workRequest)
|
||||
}
|
||||
|
||||
private fun setupFirebaseConfig() {
|
||||
remoteConfig.setConfigSettingsAsync(configSettings)
|
||||
remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
|
||||
|
||||
remoteConfig
|
||||
.fetchAndActivate()
|
||||
.addOnCompleteListener {
|
||||
if (!it.isSuccessful)
|
||||
Log.w("RemoteConfig", "Failed to fetch and activate!")
|
||||
|
||||
scheduleLinkUpdate(remoteConfig.getLong("linkUpdateDelay"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUpdate() {
|
||||
lifecycleScope.launch {
|
||||
val appVersion = (applicationContext as PolytechnicApplication).getAppVersion()
|
||||
|
||||
if (settingsDataStore.data.map { it.version }.first() != appVersion) {
|
||||
settingsDataStore.updateData { it.toBuilder().setVersion(appVersion).build() }
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
val request = OneTimeWorkRequestBuilder<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?) {
|
||||
enableEdgeToEdge()
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
schedulePeriodicRequest()
|
||||
askNotificationPermission()
|
||||
createNotificationChannels()
|
||||
|
||||
if (hasNotificationPermission())
|
||||
createNotificationChannel()
|
||||
setupFirebaseConfig()
|
||||
|
||||
handleUpdate()
|
||||
|
||||
setContent {
|
||||
Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) {
|
||||
|
||||
@@ -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
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideIn
|
||||
import androidx.compose.animation.slideOut
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -32,13 +59,21 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.n08i40k.polytechnic.next.MainViewModel
|
||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.model.UserRole
|
||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||
import ru.n08i40k.polytechnic.next.ui.MainActivity
|
||||
import ru.n08i40k.polytechnic.next.ui.icons.AppIcons
|
||||
import ru.n08i40k.polytechnic.next.ui.icons.appicons.Filled
|
||||
import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Download
|
||||
import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Telegram
|
||||
import ru.n08i40k.polytechnic.next.ui.main.profile.ProfileScreen
|
||||
import ru.n08i40k.polytechnic.next.ui.main.replacer.ReplacerScreen
|
||||
import ru.n08i40k.polytechnic.next.ui.main.schedule.ScheduleScreen
|
||||
import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
|
||||
import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel
|
||||
import ru.n08i40k.polytechnic.next.ui.model.RemoteConfigViewModel
|
||||
import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerViewModel
|
||||
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
|
||||
import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
|
||||
@@ -91,6 +126,88 @@ private fun NavHostContainer(
|
||||
})
|
||||
}
|
||||
|
||||
private fun openLink(context: Context, link: String) {
|
||||
startActivity(context, Intent(Intent.ACTION_VIEW, Uri.parse(link)), null)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LinkButton(
|
||||
text: String,
|
||||
icon: ImageVector,
|
||||
link: String,
|
||||
enabled: Boolean = true,
|
||||
badged: Boolean = false,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { openLink(context, link) },
|
||||
enabled = enabled,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
BadgedBox(badge = { if (badged) Badge() }) {
|
||||
Icon(imageVector = icon, contentDescription = text)
|
||||
}
|
||||
Spacer(Modifier.width(5.dp))
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopNavBar(
|
||||
remoteConfigViewModel: RemoteConfigViewModel
|
||||
) {
|
||||
var dropdownExpanded by remember { mutableStateOf(false) }
|
||||
val remoteConfigUiState by remoteConfigViewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
val packageVersion =
|
||||
(LocalContext.current.applicationContext as PolytechnicApplication).getAppVersion()
|
||||
val updateAvailable = remoteConfigUiState.currVersion != packageVersion
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.app_name),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { dropdownExpanded = true }) {
|
||||
BadgedBox(badge = { if (updateAvailable) Badge() }) {
|
||||
Icon(imageVector = Icons.Filled.Menu, contentDescription = "TopAppBar Menu")
|
||||
}
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = dropdownExpanded,
|
||||
onDismissRequest = { dropdownExpanded = false }
|
||||
) {
|
||||
Column(modifier = Modifier.wrapContentWidth()) {
|
||||
LinkButton(
|
||||
text = stringResource(R.string.download_update),
|
||||
icon = AppIcons.Filled.Download,
|
||||
link = remoteConfigUiState.downloadLink,
|
||||
enabled = updateAvailable,
|
||||
badged = updateAvailable
|
||||
)
|
||||
LinkButton(
|
||||
text = stringResource(R.string.telegram_channel),
|
||||
icon = AppIcons.Filled.Telegram,
|
||||
link = remoteConfigUiState.telegramLink,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomNavBar(navController: NavHostController, isAdmin: Boolean) {
|
||||
NavigationBar {
|
||||
@@ -131,26 +248,49 @@ fun MainScreen(
|
||||
if (accessToken.isEmpty()) appNavController.navigate("auth")
|
||||
}
|
||||
|
||||
val scheduleViewModel =
|
||||
hiltViewModel<ScheduleViewModel>(LocalContext.current as ComponentActivity)
|
||||
|
||||
LocalContext.current.profileViewModel =
|
||||
// profile view model
|
||||
val profileViewModel: ProfileViewModel =
|
||||
viewModel(
|
||||
factory = ProfileViewModel.provideFactory(
|
||||
profileRepository = mainViewModel.appContainer.profileRepository,
|
||||
onUnauthorized = { appNavController.navigate("auth") })
|
||||
)
|
||||
LocalContext.current.profileViewModel = profileViewModel
|
||||
|
||||
val profileUiState by LocalContext.current.profileViewModel!!.uiState.collectAsStateWithLifecycle()
|
||||
val isAdmin = (profileUiState is ProfileUiState.HasProfile) &&
|
||||
(profileUiState as ProfileUiState.HasProfile).profile.role == UserRole.ADMIN
|
||||
// remote config view model
|
||||
val remoteConfigViewModel: RemoteConfigViewModel =
|
||||
viewModel(
|
||||
factory = RemoteConfigViewModel.provideFactory(
|
||||
appContext = LocalContext.current,
|
||||
remoteConfig = (LocalContext.current as MainActivity).remoteConfig
|
||||
)
|
||||
)
|
||||
|
||||
// schedule view model
|
||||
val scheduleViewModel =
|
||||
hiltViewModel<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? =
|
||||
if (isAdmin) hiltViewModel(LocalContext.current as ComponentActivity)
|
||||
else null
|
||||
|
||||
// nav controller
|
||||
|
||||
val navController = rememberNavController()
|
||||
Scaffold(
|
||||
topBar = { TopNavBar(remoteConfigViewModel) },
|
||||
bottomBar = { BottomNavBar(navController, isAdmin) }
|
||||
) { paddingValues ->
|
||||
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;
|
||||
map<string, CachedResponse> cache_storage = 4;
|
||||
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="schedule_channel_name">Обновления расписания</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_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>
|
||||
@@ -42,7 +42,13 @@
|
||||
<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_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>
|
||||
<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>
|
||||
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>
|
||||
Reference in New Issue
Block a user