[Sync] Implement checking register availability. Improve app updates.

This commit is contained in:
Kuba Szczodrzyński 2020-09-03 13:39:46 +02:00
parent ea4591144b
commit 7bcd6bf038
24 changed files with 679 additions and 41 deletions

View File

@ -41,6 +41,8 @@ import pl.droidsonroids.gif.GifDrawable
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.* import pl.szczodrzynski.edziennik.data.api.events.*
import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Metadata.* import pl.szczodrzynski.edziennik.data.db.entity.Metadata.*
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
@ -48,7 +50,9 @@ import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding
import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent
import pl.szczodrzynski.edziennik.sync.SyncWorker import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateWorker import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.ServerMessageDialog import pl.szczodrzynski.edziennik.ui.dialogs.ServerMessageDialog
import pl.szczodrzynski.edziennik.ui.dialogs.UpdateAvailableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog
@ -438,7 +442,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}) })
b.swipeRefreshLayout.isEnabled = true b.swipeRefreshLayout.isEnabled = true
b.swipeRefreshLayout.setOnRefreshListener { this.syncCurrentFeature() } b.swipeRefreshLayout.setOnRefreshListener { launch { syncCurrentFeature() } }
b.swipeRefreshLayout.setColorSchemeResources( b.swipeRefreshLayout.setColorSchemeResources(
R.color.md_blue_500, R.color.md_blue_500,
R.color.md_amber_500, R.color.md_amber_500,
@ -604,7 +608,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|_____/ \__, |_| |_|\___| |_____/ \__, |_| |_|\___|
__/ | __/ |
|__*/ |__*/
fun syncCurrentFeature() { suspend fun syncCurrentFeature() {
if (app.profile.archived) { if (app.profile.archived) {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.profile_archived_title) .setTitle(R.string.profile_archived_title)
@ -640,6 +644,30 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
swipeRefreshLayout.isRefreshing = false swipeRefreshLayout.isRefreshing = false
return return
} }
app.profile.registerName?.let { registerName ->
var status = app.config.sync.registerAvailability[registerName]
if (status == null || status.nextCheck < currentTimeUnix()) {
withContext(Dispatchers.IO) {
val api = SzkolnyApi(app)
api.runCatching(this@MainActivity) {
val availability = getRegisterAvailability()
app.config.sync.registerAvailability = availability
status = availability[registerName]
}
}
}
if (status?.available != true
|| status?.minVersionCode ?: BuildConfig.VERSION_CODE > BuildConfig.VERSION_CODE) {
swipeRefreshLayout.isRefreshing = false
loadTarget(DRAWER_ITEM_HOME)
if (status != null)
RegisterUnavailableDialog(this, status!!)
return
}
}
swipeRefreshLayout.isRefreshing = true swipeRefreshLayout.isRefreshing = true
Toast.makeText(this, fragmentToSyncName(navTargetId), Toast.LENGTH_SHORT).show() Toast.makeText(this, fragmentToSyncName(navTargetId), Toast.LENGTH_SHORT).show()
val fragmentParam = when (navTargetId) { val fragmentParam = when (navTargetId) {
@ -656,6 +684,20 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
arguments = arguments arguments = arguments
).enqueue(this) ).enqueue(this)
} }
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onUpdateEvent(event: Update) {
EventBus.getDefault().removeStickyEvent(event)
UpdateAvailableDialog(this, event)
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) {
EventBus.getDefault().removeStickyEvent(event)
app.profile.registerName?.let { registerName ->
event.data[registerName]?.let {
RegisterUnavailableDialog(this, it)
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onApiTaskStartedEvent(event: ApiTaskStartedEvent) { fun onApiTaskStartedEvent(event: ApiTaskStartedEvent) {
swipeRefreshLayout.isRefreshing = true swipeRefreshLayout.isRefreshing = true

View File

@ -4,12 +4,18 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import pl.szczodrzynski.edziennik.config.utils.get import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.getIntList import pl.szczodrzynski.edziennik.config.utils.getIntList
import pl.szczodrzynski.edziennik.config.utils.set import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.config.utils.setMap
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
class ConfigSync(private val config: Config) { class ConfigSync(private val config: Config) {
private val gson = Gson()
private var mDontShowAppManagerDialog: Boolean? = null private var mDontShowAppManagerDialog: Boolean? = null
var dontShowAppManagerDialog: Boolean var dontShowAppManagerDialog: Boolean
get() { mDontShowAppManagerDialog = mDontShowAppManagerDialog ?: config.values.get("dontShowAppManagerDialog", false); return mDontShowAppManagerDialog ?: false } get() { mDontShowAppManagerDialog = mDontShowAppManagerDialog ?: config.values.get("dontShowAppManagerDialog", false); return mDontShowAppManagerDialog ?: false }
@ -106,4 +112,9 @@ class ConfigSync(private val config: Config) {
var tokenVulcanList: List<Int> var tokenVulcanList: List<Int>
get() { mTokenVulcanList = mTokenVulcanList ?: config.values.getIntList("tokenVulcanList", listOf()); return mTokenVulcanList ?: listOf() } get() { mTokenVulcanList = mTokenVulcanList ?: config.values.getIntList("tokenVulcanList", listOf()); return mTokenVulcanList ?: listOf() }
set(value) { config.set("tokenVulcanList", value); mTokenVulcanList = value } set(value) { config.set("tokenVulcanList", value); mTokenVulcanList = value }
private var mRegisterAvailability: Map<String, RegisterAvailabilityStatus>? = null
var registerAvailability: Map<String, RegisterAvailabilityStatus>
get() { mRegisterAvailability = mRegisterAvailability ?: config.values.get("registerAvailability", null as String?)?.let { it -> gson.fromJson<Map<String, RegisterAvailabilityStatus>>(it, object: TypeToken<Map<String, RegisterAvailabilityStatus>>(){}.type) }; return mRegisterAvailability ?: mapOf() }
set(value) { config.setMap("registerAvailability", value); mRegisterAvailability = value }
} }

View File

@ -49,6 +49,9 @@ fun AbstractConfig.setIntList(key: String, value: List<Int>?) {
fun AbstractConfig.setLongList(key: String, value: List<Long>?) { fun AbstractConfig.setLongList(key: String, value: List<Long>?) {
set(key, value?.let { gson.toJson(it) }) set(key, value?.let { gson.toJson(it) })
} }
fun <K, V> AbstractConfig.setMap(key: String, value: Map<K, V>?) {
set(key, value?.let { gson.toJson(it) })
}
fun HashMap<String, String?>.get(key: String, default: String?): String? { fun HashMap<String, String?>.get(key: String, default: String?): String? {
return this[key] ?: default return this[key] ?: default

View File

@ -5,8 +5,8 @@
package pl.szczodrzynski.edziennik.data.api.edziennik package pl.szczodrzynski.edziennik.data.api.edziennik
import com.google.gson.JsonObject import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.Edudziennik import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.Edudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.Idziennik import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.Idziennik
@ -15,9 +15,11 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.Mobidziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.Podlasie import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.Podlasie
import pl.szczodrzynski.edziennik.data.api.edziennik.template.Template import pl.szczodrzynski.edziennik.data.api.edziennik.template.Template
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.Vulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.Vulcan
import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.task.IApiTask import pl.szczodrzynski.edziennik.data.api.task.IApiTask
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
@ -88,6 +90,33 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
taskCallback.onCompleted() taskCallback.onCompleted()
return return
} }
profile.registerName?.let { registerName ->
var status = app.config.sync.registerAvailability[registerName]
if (status == null || status.nextCheck < currentTimeUnix()) {
val api = SzkolnyApi(app)
api.runCatching({
val availability = getRegisterAvailability()
app.config.sync.registerAvailability = availability
status = availability[registerName]
}, onError = {
taskCallback.onError(it.toApiError(TAG))
return
})
}
if (status?.available != true
|| status?.minVersionCode ?: BuildConfig.VERSION_CODE > BuildConfig.VERSION_CODE) {
if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) {
EventBus.getDefault().postSticky(
RegisterAvailabilityEvent(app.config.sync.registerAvailability)
)
}
cancel()
taskCallback.onCompleted()
return
}
}
} }
edziennikInterface = when (loginStore.type) { edziennikInterface = when (loginStore.type) {

View File

@ -0,0 +1,11 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-9-3.
*/
package pl.szczodrzynski.edziennik.data.api.events
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
data class RegisterAvailabilityEvent(
val data: Map< String, RegisterAvailabilityStatus>
)

View File

@ -12,12 +12,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter
import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.TimeAdapter import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.TimeAdapter
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor
import pl.szczodrzynski.edziennik.data.api.szkolny.request.* import pl.szczodrzynski.edziennik.data.api.szkolny.request.*
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse
import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Event
@ -112,6 +114,22 @@ class SzkolnyApi(val app: App) : CoroutineScope {
*/ */
@Throws(Exception::class) @Throws(Exception::class)
private inline fun <reified T> parseResponse(response: Response<ApiResponse<T>>): T { private inline fun <reified T> parseResponse(response: Response<ApiResponse<T>>): T {
app.config.update = response.body()?.update?.let { update ->
if (update.versionCode > BuildConfig.VERSION_CODE) {
if (update.updateMandatory
&& EventBus.getDefault().hasSubscriberForEvent(update::class.java)) {
EventBus.getDefault().postSticky(update)
}
update
}
else
null
}
response.body()?.registerAvailability?.let { registerAvailability ->
app.config.sync.registerAvailability = registerAvailability
}
if (response.isSuccessful && response.body()?.success == true) { if (response.isSuccessful && response.body()?.success == true) {
if (Unit is T) { if (Unit is T) {
return Unit return Unit
@ -341,4 +359,10 @@ class SzkolnyApi(val app: App) : CoroutineScope {
val response = api.firebaseToken(registerName).execute() val response = api.firebaseToken(registerName).execute()
return parseResponse(response) return parseResponse(response)
} }
@Throws(Exception::class)
fun getRegisterAvailability(): Map<String, RegisterAvailabilityStatus> {
val response = api.registerAvailability().execute()
return parseResponse(response)
}
} }

View File

@ -38,4 +38,7 @@ interface SzkolnyService {
@GET("firebase/token/{registerName}") @GET("firebase/token/{registerName}")
fun firebaseToken(@Path("registerName") registerName: String): Call<ApiResponse<String>> fun firebaseToken(@Path("registerName") registerName: String): Call<ApiResponse<String>>
@GET("registerAvailability")
fun registerAvailability(): Call<ApiResponse<Map<String, RegisterAvailabilityStatus>>>
} }

View File

@ -10,7 +10,10 @@ data class ApiResponse<T> (
val errors: List<Error>? = null, val errors: List<Error>? = null,
val data: T? = null val data: T? = null,
val update: Update? = null,
val registerAvailability: Map<String, RegisterAvailabilityStatus>? = null
) { ) {
data class Error (val code: String, val reason: String) data class Error (val code: String, val reason: String)
} }

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-9-2.
*/
package pl.szczodrzynski.edziennik.data.api.szkolny.response
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.DAY
import pl.szczodrzynski.edziennik.currentTimeUnix
data class RegisterAvailabilityStatus(
val available: Boolean,
val name: String?,
val message: Message?,
val nextCheck: Long = currentTimeUnix() + 7 * DAY,
val minVersionCode: Int = BuildConfig.VERSION_CODE
) {
data class Message(
val title: String,
val contentShort: String,
val contentLong: String,
val icon: String?,
val image: String?,
val url: String?
)
}

View File

@ -11,5 +11,6 @@ data class Update(
val releaseNotes: String?, val releaseNotes: String?,
val releaseType: String, val releaseType: String,
val isOnGooglePlay: Boolean, val isOnGooglePlay: Boolean,
val downloadUrl: String? val downloadUrl: String?,
) val updateMandatory: Boolean
)

View File

@ -16,8 +16,7 @@ import androidx.room.Ignore
import com.google.gson.JsonObject import com.google.gson.JsonObject
import pl.droidsonroids.gif.GifDrawable import pl.droidsonroids.gif.GifDrawable
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_EDUDZIENNIK import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_PODLASIE
import pl.szczodrzynski.edziennik.utils.ProfileImageHolder import pl.szczodrzynski.edziennik.utils.ProfileImageHolder
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.ImageHolder import pl.szczodrzynski.navlib.ImageHolder
@ -128,6 +127,17 @@ open class Profile(
val isParent val isParent
get() = accountName != null get() = accountName != null
val registerName
get() = when (loginStoreType) {
LOGIN_TYPE_LIBRUS -> "librus"
LOGIN_TYPE_VULCAN -> "vulcan"
LOGIN_TYPE_IDZIENNIK -> "idziennik"
LOGIN_TYPE_MOBIDZIENNIK -> "mobidziennik"
LOGIN_TYPE_PODLASIE -> "podlasie"
LOGIN_TYPE_EDUDZIENNIK -> "edudziennik"
else -> null
}
override fun getImageDrawable(context: Context): Drawable { override fun getImageDrawable(context: Context): Drawable {
if (archived) { if (archived) {
return context.getDrawableFromRes(pl.szczodrzynski.edziennik.R.drawable.profile_archived).also { return context.getDrawableFromRes(pl.szczodrzynski.edziennik.R.drawable.profile_archived).also {

View File

@ -5,10 +5,13 @@
package pl.szczodrzynski.edziennik.data.firebase package pl.szczodrzynski.edziennik.data.firebase
import com.google.gson.JsonParser import com.google.gson.JsonParser
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.events.FeedbackMessageEvent import pl.szczodrzynski.edziennik.data.api.events.FeedbackMessageEvent
import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.api.task.PostNotifications import pl.szczodrzynski.edziennik.data.api.task.PostNotifications
import pl.szczodrzynski.edziennik.data.db.entity.* import pl.szczodrzynski.edziennik.data.db.entity.*
@ -50,6 +53,16 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
val message = app.gson.fromJson(message.data.getString("message"), FeedbackMessage::class.java) ?: return@launch val message = app.gson.fromJson(message.data.getString("message"), FeedbackMessage::class.java) ?: return@launch
feedbackMessage(message) feedbackMessage(message)
} }
"registerAvailability" -> launch {
val data = app.gson.fromJson<Map<String, RegisterAvailabilityStatus>>(
message.data.getString("registerAvailability"),
object: TypeToken<Map<String, RegisterAvailabilityStatus>>(){}.type
) ?: return@launch
app.config.sync.registerAvailability = data
if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) {
EventBus.getDefault().postSticky(RegisterAvailabilityEvent(data))
}
}
} }
} }
} }

View File

@ -14,6 +14,7 @@ import android.widget.Toast
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.work.* import androidx.work.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
@ -76,7 +77,7 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker(
try { try {
val update = overrideUpdate val update = overrideUpdate
?: run { ?: run {
val updates = withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
SzkolnyApi(app).runCatching({ SzkolnyApi(app).runCatching({
getUpdate("beta") getUpdate("beta")
}, { }, {
@ -84,15 +85,25 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker(
}) })
} ?: return@run null } ?: return@run null
if (updates.isEmpty()) { if (app.config.update == null
|| app.config.update?.versionCode ?: BuildConfig.VERSION_CODE <= BuildConfig.VERSION_CODE) {
app.config.update = null app.config.update = null
Toast.makeText(app, app.getString(R.string.notification_no_update), Toast.LENGTH_SHORT).show() Toast.makeText(app, app.getString(R.string.notification_no_update), Toast.LENGTH_SHORT).show()
return@run null return@run null
} }
updates[0] app.config.update
} ?: return } ?: return
app.config.update = update if (update.versionCode <= BuildConfig.VERSION_CODE) {
app.config.update = null
return
}
if (EventBus.getDefault().hasSubscriberForEvent(update::class.java)) {
if (!update.updateMandatory) // mandatory updates are posted by the SzkolnyApi
EventBus.getDefault().postSticky(update)
return
}
val notificationIntent = Intent(app, UpdateDownloaderService::class.java) val notificationIntent = Intent(app, UpdateDownloaderService::class.java)
val pendingIntent = PendingIntent.getService(app, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT) val pendingIntent = PendingIntent.getService(app, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT)

View File

@ -0,0 +1,93 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-9-3.
*/
package pl.szczodrzynski.edziennik.ui.dialogs
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import coil.api.load
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.databinding.DialogRegisterUnavailableBinding
import pl.szczodrzynski.edziennik.onClick
import pl.szczodrzynski.edziennik.utils.Utils
import kotlin.coroutines.CoroutineContext
class RegisterUnavailableDialog(
val activity: AppCompatActivity,
val status: RegisterAvailabilityStatus,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "RegisterUnavailableDialog"
}
private lateinit var app: App
private lateinit var dialog: AlertDialog
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
init { run {
if (activity.isFinishing)
return@run
if (status.available && status.minVersionCode <= BuildConfig.VERSION_CODE)
return@run
onShowListener?.invoke(TAG)
app = activity.applicationContext as App
if (!status.available && status.message != null) {
val b = DialogRegisterUnavailableBinding.inflate(LayoutInflater.from(activity), null, false)
b.message = status.message
if (status.message.image != null)
b.image.load(status.message.image)
if (status.message.url != null) {
b.readMore.onClick {
Utils.openUrl(activity, status.message.url)
}
}
dialog = MaterialAlertDialogBuilder(activity)
.setView(b.root)
.setPositiveButton(R.string.close) { dialog, _ ->
dialog.dismiss()
}
.setOnDismissListener {
onDismissListener?.invoke(TAG)
}
.show()
}
val update = app.config.update
if (status.minVersionCode > BuildConfig.VERSION_CODE) {
if (update != null && update.versionCode >= status.minVersionCode) {
UpdateAvailableDialog(activity, update, true, onShowListener, onDismissListener)
}
else {
// this *should* never happen
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.update_available_title)
.setMessage(R.string.update_available_fallback)
.setPositiveButton(R.string.update_available_button) { dialog, _ ->
Utils.openGooglePlay(activity)
dialog.dismiss()
}
.setCancelable(false)
.setOnDismissListener {
onDismissListener?.invoke(TAG)
}
.show()
}
return@run
}
}}
}

View File

@ -17,7 +17,7 @@ import kotlin.coroutines.CoroutineContext
class ServerMessageDialog( class ServerMessageDialog(
val activity: AppCompatActivity, val activity: AppCompatActivity,
val title: String, val title: String,
val message: String, val message: CharSequence,
val onShowListener: ((tag: String) -> Unit)? = null, val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope { ) : CoroutineScope {

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-9-3.
*/
package pl.szczodrzynski.edziennik.ui.dialogs
import android.text.Html
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.sync.UpdateDownloaderService
import kotlin.coroutines.CoroutineContext
class UpdateAvailableDialog(
val activity: AppCompatActivity,
val update: Update,
val mandatory: Boolean = update.updateMandatory,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "UpdateAvailableDialog"
}
private lateinit var app: App
private lateinit var dialog: AlertDialog
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
init { run {
if (activity.isFinishing)
return@run
if (update.versionCode <= BuildConfig.VERSION_CODE)
return@run
onShowListener?.invoke(TAG)
app = activity.applicationContext as App
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.update_available_title)
.setMessage(
R.string.update_available_format,
BuildConfig.VERSION_NAME,
update.versionName,
update.releaseNotes?.let { Html.fromHtml(it) } ?: "---"
)
.setPositiveButton(R.string.update_available_button) { dialog, _ ->
activity.startService(Intent(app, UpdateDownloaderService::class.java))
dialog.dismiss()
}
.also {
if (!mandatory)
it.setNeutralButton(R.string.update_available_later, null)
}
.setCancelable(!mandatory)
.setOnDismissListener {
onDismissListener?.invoke(TAG)
}
.show()
}}
}

View File

@ -33,9 +33,10 @@ class CardItemTouchHelperCallback(private val cardAdapter: HomeCardAdapter, priv
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
removeCard(viewHolder.adapterPosition, cardAdapter) val position = viewHolder.adapterPosition
cardAdapter.items.removeAt(viewHolder.adapterPosition) removeCard(position, cardAdapter)
cardAdapter.notifyItemRemoved(viewHolder.adapterPosition) cardAdapter.items.removeAt(position)
cardAdapter.notifyItemRemoved(position)
} }
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {

View File

@ -21,12 +21,9 @@ import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon
import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont
import kotlinx.coroutines.* import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.databinding.FragmentHomeBinding import pl.szczodrzynski.edziennik.databinding.FragmentHomeBinding
import pl.szczodrzynski.edziennik.onClick
import pl.szczodrzynski.edziennik.ui.dialogs.home.StudentNumberDialog import pl.szczodrzynski.edziennik.ui.dialogs.home.StudentNumberDialog
import pl.szczodrzynski.edziennik.ui.modules.home.cards.* import pl.szczodrzynski.edziennik.ui.modules.home.cards.*
import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.Themes
@ -50,9 +47,11 @@ class HomeFragment : Fragment(), CoroutineScope {
cardAdapter.notifyItemMoved(fromPosition, toPosition) cardAdapter.notifyItemMoved(fromPosition, toPosition)
val homeCards = App.config.forProfile().ui.homeCards.toMutableList() val homeCards = App.config.forProfile().ui.homeCards.toMutableList()
val fromPair = homeCards[fromPosition] val fromIndex = homeCards.indexOfFirst { it.cardId == fromCard.id }
homeCards[fromPosition] = homeCards[toPosition] val toIndex = homeCards.indexOfFirst { it.cardId == toCard.id }
homeCards[toPosition] = fromPair val fromPair = homeCards[fromIndex]
homeCards[fromIndex] = homeCards[toIndex]
homeCards[toIndex] = fromPair
App.config.forProfile().ui.homeCards = homeCards App.config.forProfile().ui.homeCards = homeCards
return true return true
} }
@ -64,10 +63,10 @@ class HomeFragment : Fragment(), CoroutineScope {
val card = cardAdapter.items[position] val card = cardAdapter.items[position]
if (card.id >= 100) { if (card.id >= 100) {
// debug & archive cards are not removable // debug & archive cards are not removable
cardAdapter.notifyDataSetChanged() //cardAdapter.notifyDataSetChanged()
return return
} }
homeCards.removeAt(position) homeCards.removeAll { it.cardId == card.id }
App.config.forProfile().ui.homeCards = homeCards App.config.forProfile().ui.homeCards = homeCards
} }
} }
@ -166,6 +165,13 @@ class HomeFragment : Fragment(), CoroutineScope {
if (app.profile.archived) if (app.profile.archived)
items.add(0, HomeArchiveCard(101, app, activity, this, app.profile)) items.add(0, HomeArchiveCard(101, app, activity, this, app.profile))
val status = app.config.sync.registerAvailability[app.profile.registerName]
val update = app.config.update
if (update != null && update.versionCode > BuildConfig.VERSION_CODE
|| status != null && (!status.available || status.minVersionCode > BuildConfig.VERSION_CODE)) {
items.add(0, HomeAvailabilityCard(102, app, activity, this, app.profile))
}
val adapter = HomeCardAdapter(items) val adapter = HomeCardAdapter(items)
val itemTouchHelper = ItemTouchHelper(CardItemTouchHelperCallback(adapter, b.refreshLayout)) val itemTouchHelper = ItemTouchHelper(CardItemTouchHelperCallback(adapter, b.refreshLayout))
adapter.itemTouchHelper = itemTouchHelper adapter.itemTouchHelper = itemTouchHelper

View File

@ -0,0 +1,92 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-9-3.
*/
package pl.szczodrzynski.edziennik.ui.modules.home.cards
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.isVisible
import androidx.core.view.plusAssign
import androidx.core.view.setMargins
import coil.api.load
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.databinding.CardHomeAvailabilityBinding
import pl.szczodrzynski.edziennik.sync.UpdateDownloaderService
import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.UpdateAvailableDialog
import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard
import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment
import kotlin.coroutines.CoroutineContext
class HomeAvailabilityCard(
override val id: Int,
val app: App,
val activity: MainActivity,
val fragment: HomeFragment,
val profile: Profile
) : HomeCard, CoroutineScope {
companion object {
private const val TAG = "HomeAvailabilityCard"
}
private var job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun bind(position: Int, holder: HomeCardAdapter.ViewHolder) {
holder.root.removeAllViews()
val b = CardHomeAvailabilityBinding.inflate(LayoutInflater.from(holder.root.context))
b.root.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
setMargins(8.dp)
}
holder.root += b.root
val status = app.config.sync.registerAvailability[profile.registerName]
val update = app.config.update
if (update == null && status == null)
return
var onInfoClick = { _: View -> }
if (status != null && !status.available && status.message != null) {
b.homeAvailabilityTitle.text = status.message.title
b.homeAvailabilityText.text = status.message.contentShort
b.homeAvailabilityUpdate.isVisible = false
b.homeAvailabilityIcon.setImageResource(R.drawable.ic_sync)
if (status.message.icon != null)
b.homeAvailabilityIcon.load(status.message.icon)
onInfoClick = {
RegisterUnavailableDialog(activity, status)
}
}
else if (update != null && update.versionCode > BuildConfig.VERSION_CODE) {
b.homeAvailabilityTitle.setText(R.string.home_availability_title)
b.homeAvailabilityText.setText(R.string.home_availability_text, update.versionName)
b.homeAvailabilityUpdate.isVisible = true
b.homeAvailabilityIcon.setImageResource(R.drawable.ic_update)
onInfoClick = {
UpdateAvailableDialog(activity, update)
}
}
b.homeAvailabilityUpdate.onClick {
if (update == null)
return@onClick
activity.startService(Intent(app, UpdateDownloaderService::class.java))
}
b.homeAvailabilityInfo.onClick(onInfoClick)
holder.root.onClick(onInfoClick)
}
override fun unbind(position: Int, holder: HomeCardAdapter.ViewHolder) = Unit
}

View File

@ -13,14 +13,12 @@ import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers import pl.szczodrzynski.edziennik.*
import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.Bundle
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.LoginChooserFragmentBinding import pl.szczodrzynski.edziennik.databinding.LoginChooserFragmentBinding
import pl.szczodrzynski.edziennik.onClick import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog
import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackActivity import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackActivity
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -53,18 +51,23 @@ class LoginChooserFragment : Fragment(), CoroutineScope {
if (!isAdded) return if (!isAdded) return
val adapter = LoginChooserAdapter(activity) { loginType, loginMode -> val adapter = LoginChooserAdapter(activity) { loginType, loginMode ->
if (loginMode.isPlatformSelection) { launch {
nav.navigate(R.id.loginPlatformListFragment, Bundle( if (!checkAvailability(loginType.loginType))
return@launch
if (loginMode.isPlatformSelection) {
nav.navigate(R.id.loginPlatformListFragment, Bundle(
"loginType" to loginType.loginType,
"loginMode" to loginMode.loginMode
), activity.navOptions)
return@launch
}
nav.navigate(R.id.loginFormFragment, Bundle(
"loginType" to loginType.loginType, "loginType" to loginType.loginType,
"loginMode" to loginMode.loginMode "loginMode" to loginMode.loginMode
), activity.navOptions) ), activity.navOptions)
return@LoginChooserAdapter
} }
nav.navigate(R.id.loginFormFragment, Bundle(
"loginType" to loginType.loginType,
"loginMode" to loginMode.loginMode
), activity.navOptions)
} }
LoginInfo.chooserList = LoginInfo.chooserList LoginInfo.chooserList = LoginInfo.chooserList
@ -102,4 +105,35 @@ class LoginChooserFragment : Fragment(), CoroutineScope {
} }
} }
} }
private suspend fun checkAvailability(loginType: Int): Boolean {
when (loginType) {
LOGIN_TYPE_LIBRUS -> "librus"
LOGIN_TYPE_VULCAN -> "vulcan"
LOGIN_TYPE_IDZIENNIK -> "idziennik"
LOGIN_TYPE_MOBIDZIENNIK -> "mobidziennik"
LOGIN_TYPE_PODLASIE -> "podlasie"
LOGIN_TYPE_EDUDZIENNIK -> "edudziennik"
else -> null
}?.let { registerName ->
var status = app.config.sync.registerAvailability[registerName]
if (status == null || status.nextCheck < currentTimeUnix()) {
withContext(Dispatchers.IO) {
val api = SzkolnyApi(app)
api.runCatching(activity) {
val availability = getRegisterAvailability()
app.config.sync.registerAvailability = availability
status = availability[registerName]
}
}
}
if (status?.available != true) {
if (status != null)
RegisterUnavailableDialog(activity, status!!)
return false
}
}
return true
}
} }

View File

@ -0,0 +1,11 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-9-3.
-->
<vector android:height="128dp" android:viewportHeight="64"
android:viewportWidth="64" android:width="128dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#1a6ee2" android:pathData="m29.583,48.966c1.329,1.347 3.505,1.347 4.834,0l10.917,-11.06c1.466,-1.484 0.353,-3.906 -1.795,-3.906h-5.539v-8.5c0,-1.375 -1.125,-2.5 -2.5,-2.5h-7c-1.375,0 -2.5,1.125 -2.5,2.5v8.5h-5.539c-2.147,0 -3.26,2.422 -1.795,3.906z"/>
<path android:fillColor="#2aa7ed" android:pathData="m28.5,12h7c1.375,0 2.5,-1.125 2.5,-2.5s-1.125,-2.5 -2.5,-2.5h-7c-1.375,0 -2.5,1.125 -2.5,2.5s1.125,2.5 2.5,2.5z"/>
<path android:fillColor="#2082e6" android:pathData="m28.5,20h7c1.375,0 2.5,-1.125 2.5,-2.5s-1.125,-2.5 -2.5,-2.5h-7c-1.375,0 -2.5,1.125 -2.5,2.5s1.125,2.5 2.5,2.5z"/>
<path android:fillColor="#1762df" android:pathData="m54,55.5c0,1.375 -1.125,2.5 -2.5,2.5h-39c-1.375,0 -2.5,-1.125 -2.5,-2.5s1.125,-2.5 2.5,-2.5h39c1.375,0 2.5,1.125 2.5,2.5z"/>
</vector>

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-9-3.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:layout_margin="8dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/homeAvailabilityTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/home_availability_title"
android:textAppearance="@style/NavView.TextView.Title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/homeAvailabilityText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center_horizontal"
android:text="@string/home_availability_text"
android:textSize="16sp"
tools:text="Zaktualizuj aplikację do najnowszej wersji 4.3.1." />
<com.google.android.material.button.MaterialButton
android:id="@+id/homeAvailabilityUpdate"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/home_availability_update" />
<com.google.android.material.button.MaterialButton
android:id="@+id/homeAvailabilityInfo"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/home_availability_info" />
</LinearLayout>
<ImageView
android:id="@+id/homeAvailabilityIcon"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:srcCompat="@drawable/ic_update" />
</LinearLayout>
</layout>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-9-3.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<import type="android.text.Html" />
<variable
name="message"
type="pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus.Message" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitXY"
android:visibility="@{message.image != null ? View.VISIBLE : View.GONE}"
tools:src="@tools:sample/backgrounds/scenic" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="24dp"
android:paddingTop="16dp"
android:paddingRight="24dp"
android:paddingBottom="16dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{message.title}"
android:textAppearance="@style/NavView.TextView.Title"
tools:text="Dziennik nie działa" />
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@{Html.fromHtml(message.contentLong)}"
tools:text="Dziennik się zepsuł i nie działa, szkoda\n\n\nwiele linijek ma ten tekst" />
<com.google.android.material.button.MaterialButton
android:id="@+id/readMore"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:text="@string/register_unavailable_read_more"
android:visibility="@{message.url != null ? View.VISIBLE : View.GONE}" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</layout>

View File

@ -1368,4 +1368,14 @@
<string name="home_archive_close_no_target_title">Brak aktualnego profilu</string> <string name="home_archive_close_no_target_title">Brak aktualnego profilu</string>
<string name="home_archive_close_no_target_text">Uczeń %s nie posiada profilu na tym koncie w aktualnym roku szkolnym. Prawdopodobnie ten profil został usunięty lub uczeń nie uczęszcza już do tej klasy.\n\nAby przejść do aktualnego profilu, wybierz ucznia z listy lub zaloguj się na jego konto przyciskiem Dodaj ucznia.</string> <string name="home_archive_close_no_target_text">Uczeń %s nie posiada profilu na tym koncie w aktualnym roku szkolnym. Prawdopodobnie ten profil został usunięty lub uczeń nie uczęszcza już do tej klasy.\n\nAby przejść do aktualnego profilu, wybierz ucznia z listy lub zaloguj się na jego konto przyciskiem Dodaj ucznia.</string>
<string name="login_copyright_notice">Znaki towarowe zamieszczone w tej aplikacji należą do ich prawowitych właścicieli i są używane wyłącznie w celach informacyjnych.</string> <string name="login_copyright_notice">Znaki towarowe zamieszczone w tej aplikacji należą do ich prawowitych właścicieli i są używane wyłącznie w celach informacyjnych.</string>
<string name="update_available_title">Dostępna aktualiacja aplikacji</string>
<string name="update_available_format">Używasz starej wersji aplikacji Szkolny.eu (%s). Aby móc korzystać z aplikacji oraz zapewnić najlepsze działanie, zaktualizuj aplikację do wersji %s.\n\nDziennik zmian:\n%s</string>
<string name="update_available_fallback">Posiadasz nieaktualną wersję aplikacji Szkolny.eu. Aby móc dalej synchronizować dane, musisz zaktualizować aplikację.</string>
<string name="update_available_button">Aktualizuj</string>
<string name="update_available_later">Nie teraz</string>
<string name="home_availability_title">Dostępna aktualizacja</string>
<string name="home_availability_text">Zaktualizuj aplikację do najnowszej wersji %s.</string>
<string name="home_availability_info">Zobacz więcej</string>
<string name="home_availability_update">Aktualizuj</string>
<string name="register_unavailable_read_more">Dowiedz się więcej</string>
</resources> </resources>