1
0

Compare commits

..

12 Commits
2.5.0 ... 2.5.2

29 changed files with 2724 additions and 72 deletions

4
.gitignore vendored
View File

@ -67,6 +67,10 @@ captures/
.idea/discord.xml
.idea/migrations.xml
.idea/androidTestResultsUserPreferences.xml
.idea/copilot
.idea/deploymentTargetDropDown.xml
.idea/deploymentTargetSelector.xml
.idea/kotlinc.xml
# Keystore files
*.jks

View File

@ -27,8 +27,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 34
versionCode 149
versionName "2.5.0"
versionCode 151
versionName "2.5.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
@ -164,8 +164,8 @@ play {
defaultToAppBundles = false
track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.20d
updatePriority = 1
userFraction = 0.50d
updatePriority = 3
enabled.set(false)
}
@ -195,7 +195,7 @@ ext {
}
dependencies {
implementation 'io.github.wulkanowy:sdk:2.5.0'
implementation 'io.github.wulkanowy:sdk:2.5.2'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'

File diff suppressed because it is too large Load Diff

View File

@ -174,6 +174,7 @@ import javax.inject.Singleton
AutoMigration(from = 58, to = 59),
AutoMigration(from = 59, to = 60),
AutoMigration(from = 60, to = 61),
AutoMigration(from = 61, to = 62),
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -182,7 +183,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 61
const val VERSION_SCHEMA = 62
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@ -309,6 +310,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val adminMessagesDao: AdminMessageDao
abstract val mutedMessageSendersDao: MutedMessageSendersDao
abstract val gradeDescriptiveDao: GradeDescriptiveDao
}

View File

@ -9,6 +9,7 @@ import androidx.room.Transaction
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import javax.inject.Singleton
@ -23,6 +24,9 @@ abstract class StudentDao {
@Delete
abstract suspend fun delete(student: Student)
@Update(entity = Student::class)
abstract suspend fun update(studentIsAuthorized: StudentIsAuthorized)
@Update(entity = Student::class)
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)

View File

@ -78,6 +78,13 @@ data class Student(
@ColumnInfo(name = "registration_date")
val registrationDate: Instant,
@ColumnInfo(name = "is_authorized", defaultValue = "0")
val isAuthorized: Boolean,
@ColumnInfo(name = "is_edu_one", defaultValue = "0")
val isEduOne: Boolean,
) : Serializable {
@PrimaryKey(autoGenerate = true)

View File

@ -0,0 +1,16 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Entity
data class StudentIsAuthorized(
@ColumnInfo(name = "is_authorized", defaultValue = "0")
val isAuthorized: Boolean,
) : Serializable {
@PrimaryKey
var id: Long = 0
}

View File

@ -34,17 +34,19 @@ fun SdkRegisterUser.mapToPojo(password: String?) = RegisterUser(
error = it.error,
students = it.subjects
.filterIsInstance<SdkRegisterStudent>()
.map { registerSubject ->
.map { registerStudent ->
RegisterStudent(
studentId = registerSubject.studentId,
studentName = registerSubject.studentName,
studentSecondName = registerSubject.studentSecondName,
studentSurname = registerSubject.studentSurname,
className = registerSubject.className,
classId = registerSubject.classId,
isParent = registerSubject.isParent,
semesters = registerSubject.semesters
.mapToEntities(registerSubject.studentId),
studentId = registerStudent.studentId,
studentName = registerStudent.studentName,
studentSecondName = registerStudent.studentSecondName,
studentSurname = registerStudent.studentSurname,
className = registerStudent.className,
classId = registerStudent.classId,
isParent = registerStudent.isParent,
isAuthorized = registerStudent.isAuthorized,
isEduOne = registerStudent.isEduOne,
semesters = registerStudent.semesters
.mapToEntities(registerStudent.studentId),
)
},
)
@ -84,6 +86,8 @@ fun RegisterStudent.mapToStudentWithSemesters(
password = user.password.orEmpty(),
isCurrent = false,
registrationDate = Instant.now(),
isAuthorized = this.isAuthorized,
isEduOne = this.isEduOne,
).apply {
avatarColor = colors.random()
},

View File

@ -45,4 +45,6 @@ data class RegisterStudent(
val classId: Int,
val isParent: Boolean,
val semesters: List<Semester>,
val isAuthorized: Boolean,
val isEduOne: Boolean
) : java.io.Serializable

View File

@ -6,6 +6,7 @@ import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
@ -14,6 +15,7 @@ import io.github.wulkanowy.data.mappers.mapToPojo
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.getCurrentOrLast
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.security.Scrambler
import io.github.wulkanowy.utils.switchSemester
@ -100,6 +102,25 @@ class StudentRepository @Inject constructor(
return student
}
suspend fun checkCurrentStudentAuthorizationStatus() {
val student = getCurrentStudent()
if (!student.isAuthorized) {
val currentSemester = semesterDb.loadAll(
studentId = student.studentId,
classId = student.classId,
).getCurrentOrLast()
val initializedSdk = sdk.init(student).switchSemester(currentSemester)
val isAuthorized = initializedSdk.getCurrentStudent()?.isAuthorized ?: false
if (isAuthorized) {
studentDb.update(StudentIsAuthorized(isAuthorized = true).apply {
id = student.id
})
} else throw NoAuthorizationException()
}
}
suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
@ -176,3 +197,6 @@ class StudentRepository @Inject constructor(
}
}
}
class NoAuthorizationException : Exception()

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.base
import android.app.ActivityManager
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
@ -45,11 +46,19 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
themeManager.applyActivityTheme(this)
super.onCreate(savedInstanceState)
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true)
applyCustomTaskDescription()
}
@Suppress("DEPRECATION")
setTaskDescription(
ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface))
)
@Suppress("DEPRECATION")
private fun applyCustomTaskDescription() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) return
try {
val newColor = getThemeAttrColor(R.attr.colorSurface)
val taskDescription = ActivityManager.TaskDescription(null, null, newColor)
setTaskDescription(taskDescription)
} catch (e: Exception) {
Timber.e(e)
}
}
override fun showError(text: String, error: Throwable) {

View File

@ -3,7 +3,7 @@ package io.github.wulkanowy.ui.base
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.sdk.scrapper.exception.AuthorizationRequiredException
import io.github.wulkanowy.data.repositories.NoAuthorizationException
import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
@ -40,7 +40,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
is ScramblerException -> onDecryptionFailed()
is BadCredentialsException -> onExpiredCredentials()
is NoCurrentStudentException -> onNoCurrentStudent()
is AuthorizationRequiredException -> onAuthorizationRequired()
is NoAuthorizationException -> onAuthorizationRequired()
is CloudflareVerificationException -> onCaptchaVerificationRequired(error.originalUrl)
}
}

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.attendance
import android.content.res.ColorStateList
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
@ -33,17 +34,17 @@ class AttendanceAdapter @Inject constructor() :
)
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val context = holder.binding.root.context
val item = items[position]
with(holder.binding) {
attendanceItemNumber.text = item.number.toString()
attendanceItemSubject.text = item.subject.ifBlank {
root.context.getString(R.string.all_no_data)
}
attendanceItemSubject.text = item.subject
.ifBlank { context.getString(R.string.all_no_data) }
attendanceItemDescription.setText(item.descriptionRes)
attendanceItemDescription.setTextColor(
root.context.getThemeAttrColor(
context.getThemeAttrColor(
when {
item.absence && !item.excused -> R.attr.colorAttendanceAbsence
item.lateness && !item.excused -> R.attr.colorAttendanceLateness
@ -61,13 +62,15 @@ class AttendanceAdapter @Inject constructor() :
attendanceItemAlert.isVisible =
item.let { (it.absence && !it.excused) || (it.lateness && !it.excused) }
attendanceItemAlert.setColorFilter(root.context.getThemeAttrColor(
when{
item.absence && !item.excused -> R.attr.colorAttendanceAbsence
item.lateness && !item.excused -> R.attr.colorAttendanceLateness
else -> android.R.attr.colorPrimary
}
))
attendanceItemAlert.imageTintList = ColorStateList.valueOf(
context.getThemeAttrColor(
when {
item.absence && !item.excused -> R.attr.colorAttendanceAbsence
item.lateness && !item.excused -> R.attr.colorAttendanceLateness
else -> android.R.attr.colorPrimary
}
)
)
attendanceItemNumber.visibility = View.GONE
attendanceItemExcuseInfo.visibility = View.GONE
attendanceItemExcuseCheckbox.visibility = View.GONE

View File

@ -78,4 +78,9 @@ class AuthDialog : BaseDialogFragment<DialogAuthBinding>(), AuthView {
override fun showDescriptionWithName(name: String) {
binding.authDescription.text = getString(R.string.auth_description, name).parseAsHtml()
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -73,6 +73,7 @@ class MainPresenter @Inject constructor(
syncManager.startPeriodicSyncWorker()
checkAppSupport()
checkCurrentStudentAuthorizationStatus()
analytics.logEvent("app_open", "destination" to initDestination.toString())
Timber.i("Main view was initialized with $initDestination")
@ -191,4 +192,13 @@ class MainPresenter @Inject constructor(
view?.showStudentAvatar(currentStudent)
}
private fun checkCurrentStudentAuthorizationStatus() {
presenterScope.launch {
runCatching { studentRepository.checkCurrentStudentAuthorizationStatus() }
.onFailure { errorHandler.dispatch(it) }
Timber.i("Current student authorization status checked")
}
}
}

View File

@ -47,7 +47,6 @@ class MailboxChooserDialog : BaseDialogFragment<DialogMailboxChooserBinding>(),
}
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(

View File

@ -82,10 +82,10 @@ class MessagePreviewFragment :
get() = getString(R.string.message_not_exists)
companion object {
const val MESSAGE_ID_KEY = "message_id"
private const val MESSAGE_ARG_KEY = "message"
fun newInstance(message: Message) = MessagePreviewFragment().apply {
arguments = bundleOf(MESSAGE_ID_KEY to message)
arguments = bundleOf(MESSAGE_ARG_KEY to message)
}
}
@ -101,7 +101,7 @@ class MessagePreviewFragment :
messageContainer = binding.messagePreviewContainer
presenter.onAttachView(
view = this,
message = (savedInstanceState ?: arguments)?.serializable(MESSAGE_ID_KEY),
message = requireArguments().serializable(MESSAGE_ARG_KEY),
)
}
@ -233,11 +233,6 @@ class MessagePreviewFragment :
(activity as MainActivity).popView()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putSerializable(MESSAGE_ID_KEY, presenter.messageWithAttachments)
super.onSaveInstanceState(outState)
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()

View File

@ -3,10 +3,15 @@ package io.github.wulkanowy.ui.modules.message.preview
import android.annotation.SuppressLint
import androidx.core.text.parseAsHtml
import io.github.wulkanowy.R
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
@ -28,17 +33,17 @@ class MessagePreviewPresenter @Inject constructor(
private val analytics: AnalyticsHelper
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) {
var messageWithAttachments: MessageWithAttachment? = null
private var messageWithAttachments: MessageWithAttachment? = null
private lateinit var lastError: Throwable
private var retryCallback: () -> Unit = {}
fun onAttachView(view: MessagePreviewView, message: Message?) {
fun onAttachView(view: MessagePreviewView, message: Message) {
super.onAttachView(view)
view.initView()
errorHandler.showErrorMessage = ::showErrorViewOnError
loadData(requireNotNull(message))
loadData(message)
}
private fun onMessageLoadRetry(message: Message) {

View File

@ -4,30 +4,31 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import androidx.core.os.BundleCompat
import java.io.Serializable
// Even though API was introduced in 33, we use 34 as 33 is bugged in some scenarios.
inline fun <reified T : Serializable> Bundle.serializable(key: String): T = when {
Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)!!
Build.VERSION.SDK_INT >= 34 -> getSerializable(key, T::class.java)!!
else -> @Suppress("DEPRECATION") getSerializable(key) as T
}
inline fun <reified T : Serializable> Bundle.nullableSerializable(key: String): T? = when {
Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)
Build.VERSION.SDK_INT >= 34 -> getSerializable(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializable(key) as T?
}
@Suppress("UNCHECKED_CAST")
inline fun <reified T : Parcelable> Bundle.parcelableArray(key: String): Array<T>? = when {
Build.VERSION.SDK_INT >= 33 -> getParcelableArray(key, T::class.java)
else -> @Suppress("DEPRECATION") getParcelableArray(key) as Array<T>?
}
inline fun <reified T : Parcelable> Bundle.parcelableArray(key: String): Array<T>? =
BundleCompat.getParcelableArray(this, key, T::class.java) as Array<T>?
inline fun <reified T : Serializable> Intent.serializable(key: String): T = when {
Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)!!
Build.VERSION.SDK_INT >= 34 -> getSerializableExtra(key, T::class.java)!!
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T
}
inline fun <reified T : Serializable> Intent.nullableSerializable(key: String): T? = when {
Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)
Build.VERSION.SDK_INT >= 34 -> getSerializableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T?
}

View File

@ -12,6 +12,7 @@ fun Sdk.init(student: Student): Sdk {
schoolSymbol = student.schoolSymbol
studentId = student.studentId
classId = student.classId
isEduOne = student.isEduOne
emptyCookieJarInterceptor = true
if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {

View File

@ -1,11 +1,7 @@
Wersja 2.5.0
Wersja 2.5.2
dodaliśmy wyświetlanie ogłoszeń
dodaliśmy opcję przywracania wiadomości z kosza
dodaliśmy opcję wyciszania nadawców wiadomości
— naprawiliśmy opcjonalne liczenie średniej arytmetycznej, kiedy brak ocen z wagą w drugim semestrze
— usprawniliśmy ładowanie frekwencji i planu lekcji
— naprawiliśmy usprawiedliwianie nieobecności i autoryzację u użytkowników eduOne
— zmieniliśmy komunikat o zmienionym haśle
naprawiliśmy omyłkowe wyświetlanie ekranu z wymaganą autoryzacją numerem PESEL
naprawiliśmy kilka problemów ze stabilnością
poprawiliśmy wyświetlanie kolorów we frekwencji
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -852,8 +852,8 @@
<string name="auth_description">Pro provoz aplikace potřebujeme potvrdit vaši identitu. Zadejte PESEL žáka &lt;b&gt;%1$s&lt;/b&gt; v níže uvedeném poli</string>
<string name="auth_button_skip">Zatím přeskočit</string>
<!--Captcha-->
<string name="captcha_dialog_title">VULCAN\'s website requires verification</string>
<string name="captcha_dialog_description"><b>Why am I seeing this?</b>\nThe register website from which Wulkanowy downloads data displays the same screen as above, so Wulkanowy must also show it to be able to download data from this website. There\'s no way around it</string>
<string name="captcha_dialog_title">Webová stránka deníku VULCAN vyžaduje ověření</string>
<string name="captcha_dialog_description"><b>Proč se mi to zobrazuje?</b>\nWebová stránka deníku, ze které Wulkanowy stahuje data, zobrazuje stejnou obrazovku jako výše, takže Wulkanowy ji musí také zobrazit, aby bylo možné získávat data z této stránky. Nedá se to obejít</string>
<string name="captcha_verified_message">Úspěšně ověřeno</string>
<!--Errors-->
<string name="error_no_internet">Žádné internetové připojení</string>

View File

@ -852,8 +852,8 @@
<string name="auth_description">Na prevádzku aplikácie potrebujeme potvrdiť vašu identitu. Zadajte PESEL žiaka &lt;b&gt;%1$s&lt;/b&gt; v nižšie uvedenom poli</string>
<string name="auth_button_skip">Zatiaľ preskočiť</string>
<!--Captcha-->
<string name="captcha_dialog_title">VULCAN\'s website requires verification</string>
<string name="captcha_dialog_description"><b>Why am I seeing this?</b>\nThe register website from which Wulkanowy downloads data displays the same screen as above, so Wulkanowy must also show it to be able to download data from this website. There\'s no way around it</string>
<string name="captcha_dialog_title">Webová stránka denníka VULCAN vyžaduje overenie</string>
<string name="captcha_dialog_description"><b>Prečo sa mi to zobrazuje?</b>\nWebová stránka denníka, z ktorej Wulkanowy sťahuje dáta, zobrazuje rovnakú obrazovku ako vyššie, takže Wulkanowy ju musí tiež zobraziť, aby bolo možné získavať dáta z tejto stránky. Nedá sa to obísť</string>
<string name="captcha_verified_message">Úspešne overené</string>
<!--Errors-->
<string name="error_no_internet">Žiadne internetové pripojenie</string>

View File

@ -46,14 +46,15 @@
<color name="timetable_canceled_light">#d32f2f</color>
<color name="timetable_canceled_dark">#e57373</color>
<color name="timetable_change_light">#ff8f00</color>
<color name="timetable_change_dark">#ffd54f</color>
<color name="attendance_absence_light">#d32f2f</color>
<color name="attendance_absence_dark">#e57373</color>
<color name="attendance_lateness_light">#cd2a01</color>
<color name="attendance_lateness_dark">#f05d0e</color>
<color name="attendance_lateness_light">#ff8f00</color>
<color name="attendance_lateness_dark">#ffd54f</color>
<color name="colorDivider">#1f000000</color>
<color name="colorDividerInverse">#1fffffff</color>

View File

@ -5,8 +5,8 @@ import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.Sdk
import java.time.LocalDate
import java.time.Instant.now
import java.time.LocalDate
import io.github.wulkanowy.sdk.pojo.Semester as SdkSemester
fun getSemesterEntity(diaryId: Int = 1, semesterId: Int = 1, start: LocalDate = LocalDate.now(), end: LocalDate = LocalDate.now(), semesterName: Int = 1) = Semester(
@ -72,6 +72,8 @@ fun getStudentEntity(mode: Sdk.Mode = Sdk.Mode.HEBE) = Student(
symbol = "",
userLoginId = 1,
userName = "",
isEduOne = false,
isAuthorized = false
).apply {
id = 1
}

View File

@ -222,5 +222,7 @@ class GetMailboxByStudentUseCaseTest {
symbol = "",
userLoginId = 1,
userName = userName,
isAuthorized = false,
isEduOne = false
)
}

View File

@ -72,7 +72,9 @@ class GradeAverageProviderTest {
className = "",
classId = 1,
isCurrent = true,
registrationDate = Instant.now()
registrationDate = Instant.now(),
isAuthorized = false,
isEduOne = false
)
private val semesters = mutableListOf(

View File

@ -71,6 +71,8 @@ class LoginStudentSelectPresenterTest {
classId = 0,
isParent = false,
semesters = listOf(),
isEduOne = false,
isAuthorized = false,
)
private val school = RegisterUnit(

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.main
import io.github.wulkanowy.MainCoroutineRule
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager
@ -7,14 +8,23 @@ import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AdsHelper
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import io.mockk.*
import io.mockk.MockKAnnotations
import io.mockk.Runs
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.verify
import kotlinx.serialization.json.Json
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class MainPresenterTest {
@get:Rule
val coroutineRule = MainCoroutineRule()
@MockK(relaxed = true)
lateinit var errorHandler: ErrorHandler