Merge branch 'bugfix/2.5.3' into develop

This commit is contained in:
Mikołaj Pich 2024-03-25 00:05:11 +01:00
commit 7993366bfc
No known key found for this signature in database
29 changed files with 3005 additions and 116 deletions

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,15 @@
package io.github.wulkanowy.data
import com.chuckerteam.chucker.api.ChuckerInterceptor
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.StudentIsEduOne
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.RemoteConfigHelper
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -14,9 +18,12 @@ import javax.inject.Singleton
class WulkanowySdkFactory @Inject constructor(
private val chuckerInterceptor: ChuckerInterceptor,
private val remoteConfig: RemoteConfigHelper,
private val webkitCookieManagerProxy: WebkitCookieManagerProxy
private val webkitCookieManagerProxy: WebkitCookieManagerProxy,
private val studentDb: StudentDao,
) {
private val eduOneMutex = Mutex()
private val sdk = Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL
@ -30,7 +37,12 @@ class WulkanowySdkFactory @Inject constructor(
fun create() = sdk
fun create(student: Student, semester: Semester? = null): Sdk {
suspend fun create(student: Student, semester: Semester? = null): Sdk {
val overrideIsEduOne = checkEduOneAndMigrateIfNecessary(student)
return buildSdk(student, semester, overrideIsEduOne)
}
private fun buildSdk(student: Student, semester: Semester?, isStudentEduOne: Boolean): Sdk {
return create().apply {
email = student.email
password = student.password
@ -38,8 +50,8 @@ class WulkanowySdkFactory @Inject constructor(
schoolSymbol = student.schoolSymbol
studentId = student.studentId
classId = student.classId
isEduOne = student.isEduOne
emptyCookieJarInterceptor = true
isEduOne = isStudentEduOne
if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {
mobileBaseUrl = student.mobileBaseUrl
@ -62,4 +74,40 @@ class WulkanowySdkFactory @Inject constructor(
}
}
}
private suspend fun checkEduOneAndMigrateIfNecessary(student: Student): Boolean {
if (student.isEduOne != null) return student.isEduOne
eduOneMutex.withLock {
val studentFromDatabase = studentDb.loadById(student.id)
if (studentFromDatabase?.isEduOne != null) {
Timber.d("Migration eduOne: already done")
return studentFromDatabase.isEduOne
}
Timber.d("Migration eduOne: flag missing. Running migration...")
val initializedSdk = buildSdk(
student = student,
semester = null,
isStudentEduOne = false, // doesn't matter
)
val newCurrentStudent = runCatching { initializedSdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Migration eduOne: can't get current student") }
.getOrNull()
if (newCurrentStudent == null) {
Timber.d("Migration eduOne: failed, so skipping")
return false
}
Timber.d("Migration eduOne: success. New isEduOne flag: ${newCurrentStudent.isEduOne}")
val studentIsEduOne = StudentIsEduOne(
id = student.id,
isEduOne = newCurrentStudent.isEduOne
)
studentDb.update(studentIsEduOne)
return newCurrentStudent.isEduOne
}
}
}

View File

@ -120,6 +120,7 @@ import io.github.wulkanowy.data.db.migrations.Migration55
import io.github.wulkanowy.data.db.migrations.Migration57
import io.github.wulkanowy.data.db.migrations.Migration58
import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration63
import io.github.wulkanowy.data.db.migrations.Migration7
import io.github.wulkanowy.data.db.migrations.Migration8
import io.github.wulkanowy.data.db.migrations.Migration9
@ -175,6 +176,7 @@ import javax.inject.Singleton
AutoMigration(from = 59, to = 60),
AutoMigration(from = 60, to = 61),
AutoMigration(from = 61, to = 62),
AutoMigration(from = 62, to = 63, spec = Migration63::class),
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -183,7 +185,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 62
const val VERSION_SCHEMA = 63
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),

View File

@ -10,6 +10,7 @@ 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.StudentIsEduOne
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import javax.inject.Singleton
@ -27,6 +28,9 @@ abstract class StudentDao {
@Update(entity = Student::class)
abstract suspend fun update(studentIsAuthorized: StudentIsAuthorized)
@Update(entity = Student::class)
abstract suspend fun update(studentIsEduOne: StudentIsEduOne)
@Update(entity = Student::class)
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)

View File

@ -82,8 +82,8 @@ data class Student(
@ColumnInfo(name = "is_authorized", defaultValue = "0")
val isAuthorized: Boolean,
@ColumnInfo(name = "is_edu_one", defaultValue = "0")
val isEduOne: Boolean,
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
val isEduOne: Boolean?,
) : Serializable {
@ -95,3 +95,22 @@ data class Student(
@ColumnInfo(name = "avatar_color")
var avatarColor = 0L
}
@Entity
data class StudentIsAuthorized(
@PrimaryKey
var id: Long,
@ColumnInfo(name = "is_authorized", defaultValue = "NULL")
val isAuthorized: Boolean?,
) : Serializable
@Entity
data class StudentIsEduOne(
@PrimaryKey
var id: Long,
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
val isEduOne: Boolean?,
) : Serializable

View File

@ -1,16 +0,0 @@
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

@ -0,0 +1,11 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.AutoMigrationSpec
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration63 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
db.execSQL("UPDATE Students SET is_edu_one = NULL WHERE is_edu_one = 0")
}
}

View File

@ -12,13 +12,14 @@ import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.mappers.mapToEntities
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.security.Scrambler
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -101,23 +102,44 @@ class StudentRepository @Inject constructor(
return student
}
suspend fun checkCurrentStudentAuthorizationStatus() {
suspend fun updateCurrentStudentAuthStatus() {
Timber.i("Check isAuthorized: started")
val student = getCurrentStudent()
if (!student.isAuthorized) {
val currentSemester = semesterDb.loadAll(
studentId = student.studentId,
classId = student.classId,
).getCurrentOrLast()
val initializedSdk = wulkanowySdkFactory.create(student, currentSemester)
val isAuthorized = initializedSdk.getCurrentStudent()?.isAuthorized ?: false
if (isAuthorized) {
studentDb.update(StudentIsAuthorized(isAuthorized = true).apply {
id = student.id
})
} else throw NoAuthorizationException()
if (student.isAuthorized) {
Timber.i("Check isAuthorized: already authorized")
return
}
val initializedSdk = wulkanowySdkFactory.create(student)
val newCurrentStudent = runCatching { initializedSdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Check isAuthorized: error occurred") }
.getOrNull()
if (newCurrentStudent == null) {
Timber.d("Check isAuthorized: current user is null")
return
}
val currentStudentSemesters = semesterDb.loadAll(student.studentId, student.classId)
if (currentStudentSemesters.isEmpty()) {
Timber.d("Check isAuthorized: apply empty semesters workaround")
semesterDb.insertSemesters(
items = newCurrentStudent.semesters.mapToEntities(student.studentId),
)
}
if (!newCurrentStudent.isAuthorized) {
Timber.i("Check isAuthorized: authorization required")
throw NoAuthorizationException()
}
val studentIsAuthorized = StudentIsAuthorized(
id = student.id,
isAuthorized = true
)
Timber.i("Check isAuthorized: already authorized, update local status")
studentDb.update(studentIsAuthorized)
}
suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
@ -172,15 +194,19 @@ class StudentRepository @Inject constructor(
wulkanowySdkFactory.create(student, semester)
.authorizePermission(pesel)
suspend fun refreshStudentName(student: Student, semester: Semester) {
suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) {
val newCurrentApiStudent = wulkanowySdkFactory.create(student, semester)
.getCurrentStudent() ?: return
.getCurrentStudent() ?: return Timber.d("Can't find student with id ${student.studentId}")
val studentName = StudentName(
studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}"
).apply { id = student.id }
studentDb.update(studentName)
semesterDb.removeOldAndSaveNew(
oldItems = semesterDb.loadAll(student.studentId, semester.classId),
newItems = newCurrentApiStudent.semesters.mapToEntities(newCurrentApiStudent.studentId)
)
}
suspend fun deleteStudentsAssociatedWithAccount(student: Student) {
@ -196,4 +222,3 @@ class StudentRepository @Inject constructor(
}
class NoAuthorizationException : Exception()

View File

@ -17,6 +17,7 @@ import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureUnavailableException
import io.github.wulkanowy.services.sync.channels.DebugChannel
import io.github.wulkanowy.services.sync.works.Work
import io.github.wulkanowy.utils.DispatchersProvider
@ -48,6 +49,7 @@ class SyncWorker @AssistedInject constructor(
val semester = semesterRepository.getCurrentSemester(student, true)
student to semester
} catch (e: Throwable) {
Timber.e(e)
return@withContext getResultFromErrors(listOf(e))
}
@ -59,7 +61,7 @@ class SyncWorker @AssistedInject constructor(
null
} catch (e: Throwable) {
Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred")
if (e is FeatureDisabledException || e is FeatureNotAvailableException) {
if (e is FeatureDisabledException || e is FeatureNotAvailableException || e is FeatureUnavailableException) {
null
} else {
Timber.e(e)

View File

@ -5,6 +5,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class AuthPresenter @Inject constructor(
@ -57,8 +58,9 @@ class AuthPresenter @Inject constructor(
val semester = semesterRepository.getCurrentSemester(student)
val isSuccess = studentRepository.authorizePermission(student, semester, pesel)
Timber.d("Auth succeed: $isSuccess")
if (isSuccess) {
studentRepository.refreshStudentName(student, semester)
studentRepository.refreshStudentAfterAuthorize(student, semester)
}
isSuccess
}
@ -68,6 +70,7 @@ class AuthPresenter @Inject constructor(
view?.showContent(true)
}
.onSuccess {
Timber.d("Auth fully succeed: $it")
if (it) {
view?.showSuccess(true)
view?.showContent(false)

View File

@ -7,7 +7,6 @@ import android.view.MenuItem
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
@ -31,14 +30,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
@Inject
lateinit var presenter: GradePresenter
private val pagerAdapter by lazy {
BaseFragmentPagerAdapter(
fragmentManager = childFragmentManager,
pagesCount = 3,
lifecycle = lifecycle,
)
}
private var semesterSwitchMenu: MenuItem? = null
companion object {
@ -52,6 +43,8 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override val currentPageIndex get() = binding.gradeViewPager.currentItem
private var pagerAdapter: BaseFragmentPagerAdapter? = null
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -71,13 +64,26 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
override fun initView() {
with(binding) {
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
}
override fun initTabs(pageCount: Int) {
pagerAdapter = BaseFragmentPagerAdapter(
lifecycle = lifecycle,
pagesCount = pageCount,
fragmentManager = childFragmentManager
)
with(binding.gradeViewPager) {
adapter = pagerAdapter
offscreenPageLimit = 3
setOnSelectPageListener(presenter::onPageSelected)
}
with(pagerAdapter) {
with(pagerAdapter!!) {
containerId = binding.gradeViewPager.id
titleFactory = {
when (it) {
@ -99,11 +105,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
binding.gradeTabLayout.elevation = requireContext().dpToPx(4f)
with(binding) {
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -169,19 +170,20 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
override fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)
?.onParentLoadData(semesterId, forceRefresh)
}
override fun notifyChildParentReselected(index: Int) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentReselected()
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentReselected()
}
override fun notifyChildSemesterChange(index: Int) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
}
override fun onDestroyView() {
pagerAdapter = null
presenter.onDetachView()
super.onDestroyView()
}

View File

@ -22,11 +22,8 @@ class GradePresenter @Inject constructor(
) : BasePresenter<GradeView>(errorHandler, studentRepository) {
private var selectedIndex = 0
private var schoolYear = 0
private var semesters = emptyList<Semester>()
private var availableSemesters = emptyList<Semester>()
private val loadedSemesterId = mutableMapOf<Int, Int>()
private lateinit var lastError: Throwable
@ -40,7 +37,7 @@ class GradePresenter @Inject constructor(
}
fun onCreateMenu() {
if (semesters.isEmpty()) view?.showSemesterSwitch(false)
if (availableSemesters.isEmpty()) view?.showSemesterSwitch(false)
}
fun onViewReselected() {
@ -49,8 +46,8 @@ class GradePresenter @Inject constructor(
}
fun onSemesterSwitch(): Boolean {
if (semesters.isNotEmpty()) {
view?.showSemesterDialog(selectedIndex - 1, semesters.take(2))
if (availableSemesters.isNotEmpty()) {
view?.showSemesterDialog(selectedIndex - 1, availableSemesters.take(2))
}
return true
}
@ -83,7 +80,7 @@ class GradePresenter @Inject constructor(
}
fun onPageSelected(index: Int) {
if (semesters.isNotEmpty()) loadChild(index)
if (availableSemesters.isNotEmpty()) loadChild(index)
}
fun onRetry() {
@ -101,16 +98,24 @@ class GradePresenter @Inject constructor(
private fun loadData() {
resourceFlow {
val student = studentRepository.getCurrentStudent()
semesterRepository.getSemesters(student, refreshOnNoCurrent = true)
val semesters = semesterRepository.getSemesters(student, refreshOnNoCurrent = true)
student to semesters
}
.logResourceStatus("load grade data")
.onResourceData {
val current = it.getCurrentOrLast()
selectedIndex = if (selectedIndex == 0) current.semesterName else selectedIndex
schoolYear = current.schoolYear
semesters = it.filter { semester -> semester.diaryId == current.diaryId }
view?.setCurrentSemesterName(current.semesterName, schoolYear)
.onResourceData { (student, semesters) ->
val currentSemester = semesters.getCurrentOrLast()
selectedIndex =
if (selectedIndex == 0) currentSemester.semesterName else selectedIndex
schoolYear = currentSemester.schoolYear
availableSemesters = semesters.filter { semester ->
semester.diaryId == currentSemester.diaryId
}
view?.run {
initTabs(if (student.isEduOne == true) 2 else 3)
setCurrentSemesterName(currentSemester.semesterName, schoolYear)
Timber.i("Loading grade data: Attempt load index $currentPageIndex")
loadChild(currentPageIndex)
showErrorView(false)
@ -131,10 +136,10 @@ class GradePresenter @Inject constructor(
}
private fun loadChild(index: Int, forceRefresh: Boolean = false) {
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${semesters.joinToString { it.semesterName.toString() }}")
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${availableSemesters.joinToString { it.semesterName.toString() }}")
val newSelectedSemesterId = try {
semesters.first { it.semesterName == selectedIndex }.semesterId
availableSemesters.first { it.semesterName == selectedIndex }.semesterId
} catch (e: NoSuchElementException) {
Timber.e(e, "Selected semester no exists")
return

View File

@ -9,6 +9,8 @@ interface GradeView : BaseView {
fun initView()
fun initTabs(pageCount: Int)
fun showContent(show: Boolean)
fun showProgress(show: Boolean)

View File

@ -98,8 +98,10 @@ class HomeworkAddDialog : BaseDialogFragment<DialogHomeworkAddBinding>(), Homewo
rangeEnd = LocalDate.now().lastSchoolDayInSchoolYear,
onDateSelected = {
date = it
if (isAdded) {
binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString())
}
}
)
}

View File

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

View File

@ -27,7 +27,7 @@ class TimetableAdapter @Inject constructor() :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (TimetableItemType.values()[viewType]) {
return when (TimetableItemType.entries[viewType]) {
TimetableItemType.SMALL -> SmallViewHolder(
ItemTimetableSmallBinding.inflate(inflater, parent, false)
)
@ -79,6 +79,7 @@ class TimetableAdapter @Inject constructor() :
with(binding) {
timetableSmallItemNumber.text = lesson.number.toString()
timetableSmallItemNumber.isVisible = item.isLessonNumberVisible
timetableSmallItemSubject.text = lesson.subject
timetableSmallItemTimeStart.text = lesson.start.toFormattedString("HH:mm")
timetableSmallItemRoom.text = lesson.room
@ -97,6 +98,7 @@ class TimetableAdapter @Inject constructor() :
with(binding) {
timetableItemNumber.text = lesson.number.toString()
timetableItemNumber.isVisible = item.isLessonNumberVisible
timetableItemSubject.text = lesson.subject
timetableItemGroup.text = lesson.group
timetableItemRoom.text = lesson.room

View File

@ -7,12 +7,14 @@ sealed class TimetableItem(val type: TimetableItemType) {
data class Small(
val lesson: Timetable,
val isLessonNumberVisible: Boolean,
val onClick: (Timetable) -> Unit,
) : TimetableItem(TimetableItemType.SMALL)
data class Normal(
val lesson: Timetable,
val showGroupsInPlan: Boolean,
val isLessonNumberVisible: Boolean,
val timeLeft: TimeLeft?,
val onClick: (Timetable) -> Unit,
) : TimetableItem(TimetableItemType.NORMAL)

View File

@ -57,6 +57,7 @@ class TimetablePresenter @Inject constructor(
private var initialDate: LocalDate? = null
private var isWeekendHasLessons: Boolean = false
private var isEduOne: Boolean = false
var currentDate: LocalDate? = null
private set
@ -149,6 +150,7 @@ class TimetablePresenter @Inject constructor(
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
isEduOne = student.isEduOne == true
checkInitialAndCurrentDate(semester)
timetableRepository.getTimetable(
student = student,
@ -234,9 +236,8 @@ class TimetablePresenter @Inject constructor(
if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) {
it.isStudentPlan
} else true
}.sortedWith(
compareBy({ item -> item.number }, { item -> !item.isStudentPlan })
)
}
.sortedWith(compareBy({ item -> item.start }, { item -> !item.isStudentPlan }))
var prevNum = when (prefRepository.showTimetableGaps) {
BETWEEN_AND_BEFORE_LESSONS -> 0
@ -257,13 +258,15 @@ class TimetablePresenter @Inject constructor(
lesson = it,
showGroupsInPlan = prefRepository.showGroupsInPlan,
timeLeft = filteredItems.getTimeLeftForLesson(it, i),
onClick = ::onTimetableItemSelected
onClick = ::onTimetableItemSelected,
isLessonNumberVisible = !isEduOne
)
add(normalLesson)
} else {
val smallLesson = TimetableItem.Small(
lesson = it,
onClick = ::onTimetableItemSelected
onClick = ::onTimetableItemSelected,
isLessonNumberVisible = !isEduOne
)
add(smallLesson)
}

View File

@ -46,11 +46,8 @@ class TimetableWidgetFactory(
) : RemoteViewsService.RemoteViewsFactory {
private var items = emptyList<TimetableWidgetItem>()
private var timetableCanceledColor: Int? = null
private var textColor: Int? = null
private var timetableChangeColor: Int? = null
override fun getLoadingView() = null
@ -81,7 +78,7 @@ class TimetableWidgetFactory(
val lessons = getLessons(student, semester, date)
val lastSync = timetableRepository.getLastRefreshTimestamp(semester, date, date)
createItems(lessons, lastSync)
createItems(lessons, lastSync, !(student.isEduOne ?: false))
}
.onFailure {
items = listOf(TimetableWidgetItem.Error(it))
@ -113,12 +110,13 @@ class TimetableWidgetFactory(
isFromAppWidget = true
)
val lessons = timetable.toFirstResult().dataOrThrow.lessons
return lessons.sortedBy { it.number }
return lessons.sortedBy { it.start }
}
private fun createItems(
lessons: List<Timetable>,
lastSync: Instant?,
isEduOne: Boolean
): List<TimetableWidgetItem> {
var prevNum = when (prefRepository.showTimetableGaps) {
BETWEEN_AND_BEFORE_LESSONS -> 0
@ -134,7 +132,7 @@ class TimetableWidgetFactory(
)
add(emptyItem)
}
add(TimetableWidgetItem.Normal(it))
add(TimetableWidgetItem.Normal(it, isEduOne))
prevNum = it.number
}
add(TimetableWidgetItem.Synchronized(lastSync ?: Instant.MIN))
@ -162,9 +160,11 @@ class TimetableWidgetFactory(
val lessonStartTime = lesson.start.toFormattedString(TIME_FORMAT_STYLE)
val lessonEndTime = lesson.end.toFormattedString(TIME_FORMAT_STYLE)
val lessonNumberVisibility = if (item.isLessonNumberVisible) VISIBLE else GONE
val remoteViews = RemoteViews(context.packageName, R.layout.item_widget_timetable).apply {
setTextViewText(R.id.timetableWidgetItemNumber, lesson.number.toString())
setViewVisibility(R.id.timetableWidgetItemNumber, lessonNumberVisibility)
setTextViewText(R.id.timetableWidgetItemTimeStart, lessonStartTime)
setTextViewText(R.id.timetableWidgetItemTimeFinish, lessonEndTime)
setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject)

View File

@ -7,6 +7,7 @@ sealed class TimetableWidgetItem(val type: TimetableWidgetItemType) {
data class Normal(
val lesson: Timetable,
val isLessonNumberVisible: Boolean,
) : TimetableWidgetItem(TimetableWidgetItemType.NORMAL)
data class Empty(

View File

@ -1,7 +1,8 @@
Wersja 2.5.2
Wersja 2.5.3
— 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
— naprawiliśmy wyświetlanie błędu "Brak uprawnień" po starcie aplikacji u użytkowników eduOne
— naprawiliśmy obsługę autoryzacji u użytkowników eduOne
— ukryliśmy numery lekcji i oceny klasy u użytkowników eduOne, bo VULCAN te funkcje usunął
— naprawiliśmy inne rzeczy u użytkowników eduOne (jak brak opisu oceny czy ładowanie danych na kilku ekranach)
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -93,9 +93,13 @@ class AdsHelper @Inject constructor(
private fun initializeMobileAds() {
if (isMobileAdsInitializeCalled.getAndSet(true)) return
try {
MobileAds.initialize(context) {
isMobileAdsSdkInitialized.value = true
}
} catch (e: Exception) {
Timber.e(e)
}
}
suspend fun getSupportAd(): RewardedInterstitialAd? {

View File

@ -2,11 +2,12 @@ package io.github.wulkanowy
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.sdk.Sdk
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
fun createWulkanowySdkFactoryMock(sdk: Sdk) = mockk<WulkanowySdkFactory>()
.apply {
every { create() } returns sdk
every { create(any(), any()) } answers { callOriginal() }
coEvery { create(any(), any()) } returns sdk
}

View File

@ -0,0 +1,129 @@
package io.github.wulkanowy.data
import android.os.Build
import dagger.hilt.android.testing.HiltTestApplication
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.getStudentEntity
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.RegisterStudent
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.O_MR1], application = HiltTestApplication::class)
class WulkanowySdkFactoryTest {
private lateinit var wulkanowySdkFactory: WulkanowySdkFactory
private lateinit var studentDao: StudentDao
private lateinit var sdk: Sdk
@Before
fun setUp() {
sdk = mockk(relaxed = true)
studentDao = mockk()
wulkanowySdkFactory = spyk(
WulkanowySdkFactory(
chuckerInterceptor = mockk(),
remoteConfig = mockk(relaxed = true),
webkitCookieManagerProxy = mockk(),
studentDb = studentDao
)
)
every { wulkanowySdkFactory.create() } returns sdk
}
@Test
fun `check sdk flag isEduOne when local student is eduone`() = runTest {
val student = getStudentEntity().copy(isEduOne = true)
wulkanowySdkFactory.create(student)
verify { sdk.isEduOne = true }
coVerify(exactly = 0) { sdk.getCurrentStudent() }
}
@Test
fun `check sdk flag isEduOne when local student is not eduone`() = runTest {
val student = getStudentEntity().copy(isEduOne = false)
wulkanowySdkFactory.create(student)
verify { sdk.isEduOne = false }
coVerify(exactly = 0) { sdk.getCurrentStudent() }
}
@Test
fun `check sdk flag isEduOne when local student is eduone null and remote student is eduone true`() =
runTest {
val studentToProcess = getStudentEntity().copy(isEduOne = null)
val registerStudent = studentToProcess.toRegisterStudent(isEduOne = true)
coEvery { studentDao.loadById(any()) } returns studentToProcess
coEvery { studentDao.update(any(StudentIsEduOne::class)) } just Runs
coEvery { sdk.getCurrentStudent() } returns registerStudent
wulkanowySdkFactory.create(studentToProcess)
verify { sdk.isEduOne = true }
coVerify { sdk.getCurrentStudent() }
}
@Test
fun `check sdk flag isEduOne when local student is eduone null and remote student is eduone false`() =
runTest {
val studentToProcess = getStudentEntity().copy(isEduOne = null)
val registerStudent = studentToProcess.toRegisterStudent(isEduOne = false)
coEvery { studentDao.loadById(any()) } returns studentToProcess
coEvery { studentDao.update(any(StudentIsEduOne::class)) } just Runs
coEvery { sdk.getCurrentStudent() } returns registerStudent
wulkanowySdkFactory.create(studentToProcess)
verify { sdk.isEduOne = false }
coVerify { sdk.getCurrentStudent() }
}
@Test
fun `check sdk flag isEduOne when sdk getCurrentStudent throws error`() =
runTest {
val studentToProcess = getStudentEntity().copy(isEduOne = null)
coEvery { studentDao.loadById(any()) } returns studentToProcess
coEvery { studentDao.update(any(StudentIsEduOne::class)) } just Runs
coEvery { sdk.getCurrentStudent() } throws Exception()
wulkanowySdkFactory.create(studentToProcess)
verify { sdk.isEduOne = false }
coVerify { sdk.getCurrentStudent() }
}
private fun Student.toRegisterStudent(isEduOne: Boolean) = RegisterStudent(
studentId = studentId,
studentName = studentName,
studentSecondName = studentName,
studentSurname = studentName,
className = className,
classId = classId,
isParent = isParent,
isAuthorized = isAuthorized,
semesters = emptyList(),
isEduOne = isEduOne,
)
}

View File

@ -21,10 +21,10 @@ abstract class AbstractMigrationTest {
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java,
listOf(Migration55()),
FrameworkSQLiteOpenHelperFactory()
instrumentation = InstrumentationRegistry.getInstrumentation(),
databaseClass = AppDatabase::class.java,
specs = listOf(Migration63()),
openFactory = FrameworkSQLiteOpenHelperFactory()
)
fun runMigrationsAndValidate(migration: Migration) {

View File

@ -7,8 +7,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.Sdk.ScrapperLoginType.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import io.github.wulkanowy.sdk.Sdk.ScrapperLoginType.ADFSLight
import io.github.wulkanowy.sdk.Sdk.ScrapperLoginType.ADFSLightScoped
import io.github.wulkanowy.sdk.Sdk.ScrapperLoginType.STANDARD
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@ -19,7 +20,6 @@ import kotlin.test.assertEquals
@HiltAndroidTest
@RunWith(RobolectricTestRunner::class)
@OptIn(ExperimentalCoroutinesApi::class)
@Config(sdk = [Build.VERSION_CODES.O_MR1], application = HiltTestApplication::class)
class Migration54Test : AbstractMigrationTest() {

View File

@ -0,0 +1,89 @@
package io.github.wulkanowy.data.db.migrations
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import android.os.Build
import androidx.sqlite.db.SupportSQLiteDatabase
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import kotlinx.coroutines.test.runTest
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
@HiltAndroidTest
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.O_MR1], application = HiltTestApplication::class)
class Migration63Test : AbstractMigrationTest() {
@Test
fun `update is_edu_one to null if 0`() = runTest {
with(helper.createDatabase(dbName, 62)) {
createStudent(1, 0)
close()
}
helper.runMigrationsAndValidate(dbName, 63, true)
val database = getMigratedRoomDatabase()
val studentDb = database.studentDao
val student = studentDb.loadById(1)
assertNull(student!!.isEduOne)
database.close()
}
@Test
fun `check is_edu_one is stay same`() = runTest {
with(helper.createDatabase(dbName, 62)) {
createStudent(1, 1)
close()
}
helper.runMigrationsAndValidate(dbName, 63, true)
val database = getMigratedRoomDatabase()
val studentDb = database.studentDao
val student = studentDb.loadById(1)
val isEduOne = assertNotNull(student!!.isEduOne)
assertTrue(isEduOne)
database.close()
}
private fun SupportSQLiteDatabase.createStudent(id: Long, isEduOneValue: Int) {
insert("Students", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
put("scrapper_base_url", "https://fakelog.cf")
put("mobile_base_url", "")
put("login_type", "SCRAPPER")
put("login_mode", "SCRAPPER")
put("certificate_key", "")
put("private_key", "")
put("is_parent", false)
put("email", "jan@fakelog.cf")
put("password", "******")
put("symbol", "symbol")
put("student_id", Random.nextInt())
put("user_login_id", 123)
put("user_name", "studentName")
put("student_name", "studentName")
put("school_id", "123")
put("school_short", "")
put("school_name", "")
put("class_name", "")
put("class_id", Random.nextInt())
put("is_current", false)
put("registration_date", "0")
put("id", id)
put("nick", "")
put("avatar_color", "")
put("is_edu_one", isEduOneValue)
})
}
}

View File

@ -48,8 +48,11 @@ class LuckyNumberRemoteTest {
fun setUp() {
MockKAnnotations.init(this)
luckyNumberRepository =
LuckyNumberRepository(luckyNumberDb, wulkanowySdkFactory, appWidgetUpdater)
luckyNumberRepository = LuckyNumberRepository(
luckyNumberDb = luckyNumberDb,
wulkanowySdkFactory = wulkanowySdkFactory,
appWidgetUpdater = appWidgetUpdater,
)
}
@Test