Compare commits

...

27 Commits

Author SHA1 Message Date
440cc7ae89 Merge branch 'release/2.6.8' 2024-05-17 07:38:46 +02:00
9a6b17c9d9 Version 2.6.8 2024-05-17 07:38:13 +02:00
729e0f547b Add sandbox isolate 2024-05-16 23:23:06 +02:00
faa8d34e79 Bump sdk to 2.6.7-SNAPSHOT 2024-05-16 18:55:02 +02:00
b6f5ac91ad Merge branch 'release/2.6.7' into develop 2024-05-16 08:50:37 +02:00
51dd343299 Merge branch 'release/2.6.7' 2024-05-16 08:50:33 +02:00
d1d0caa1e3 Version 2.6.7 2024-05-16 08:50:28 +02:00
c40779f48f Merge branch 'release/2.6.6' into develop 2024-05-14 22:31:01 +02:00
5ee7fee09d Merge branch 'release/2.6.6' 2024-05-14 22:30:57 +02:00
594d2dbec5 Version 2.6.6 2024-05-14 22:30:51 +02:00
26596e8254 Merge branch 'release/2.6.5' into develop 2024-05-12 17:51:07 +02:00
ff56348586 Merge branch 'release/2.6.5' 2024-05-12 17:51:02 +02:00
6e472d6c5c Version 2.6.5 2024-05-12 17:50:56 +02:00
6f4826249c Add remote mapping for Wulkanowy SDK (#2550)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-05-12 16:21:28 +02:00
2f3e1b6aae Merge branch 'release/2.6.4' into develop 2024-05-10 12:35:39 +02:00
a69361708e Merge branch 'release/2.6.4' 2024-05-10 12:34:11 +02:00
567d868f76 Version 2.6.4 2024-05-10 12:30:30 +02:00
12030efee2 Version 2.6.3 2024-05-09 22:59:20 +02:00
3cbe98d7b8 Merge branch 'release/2.6.3' 2024-05-09 22:58:03 +02:00
503a97bc4c Version 2.6.3 2024-05-09 22:57:58 +02:00
04c382643d Fix a typo in settings - apperance & behaviour - menu configuration (#2547) 2024-05-09 18:17:10 +02:00
fc140ad9c1 Merge branch 'release/2.6.2' into develop 2024-05-08 01:55:42 +02:00
ae6a35121b Merge branch 'release/2.6.2' 2024-05-08 01:55:38 +02:00
78a2cc89e9 Version 2.6.2 2024-05-08 01:55:09 +02:00
558addd097 Merge branch 'release/2.6.1' 2024-05-02 15:20:51 +02:00
e8f9c57c34 Merge branch 'release/2.6.0' 2024-05-01 22:30:48 +02:00
4d67de8e5f Merge branch 'bugfix/2.5.8' 2024-04-25 12:45:19 +02:00
23 changed files with 165 additions and 38 deletions

1
.gitignore vendored
View File

@ -127,3 +127,4 @@ google-services.json
!app/google-services.json
.idea/appInsightsSettings.xml

View File

@ -27,8 +27,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 34
versionCode 161
versionName "2.6.1"
versionCode 168
versionName "2.6.8"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
@ -160,8 +160,8 @@ play {
defaultToAppBundles = false
track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.25d
updatePriority = 1
userFraction = 0.99d
updatePriority = 2
enabled.set(false)
}
@ -191,12 +191,13 @@ ext {
}
dependencies {
implementation 'io.github.wulkanowy:sdk:2.6.0'
implementation 'io.github.wulkanowy:sdk:2.6.7'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines"
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.core:core-splashscreen:1.0.1'
@ -204,6 +205,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.7.0"
implementation "androidx.annotation:annotation:1.7.1"
implementation "androidx.javascriptengine:javascriptengine:1.0.0-beta01"
implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.recyclerview:recyclerview:1.3.2"

View File

@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly">
<uses-sdk tools:overrideLibrary="androidx.javascriptengine" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
@ -42,9 +44,9 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:resizeableActivity="true"
android:supportsRtl="false"
android:theme="@style/WulkanowyTheme"
android:resizeableActivity="true"
tools:ignore="DataExtractionRules,UnusedAttribute">
<activity
android:name=".ui.modules.splash.SplashActivity"

View File

@ -13,8 +13,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.api.services.SchoolsService
import io.github.wulkanowy.data.api.services.WulkanowyService
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
@ -71,7 +71,7 @@ internal class DataModule {
okHttpClient: OkHttpClient,
json: Json,
appInfo: AppInfo
): AdminMessageService = Retrofit.Builder()
): WulkanowyService = Retrofit.Builder()
.baseUrl(appInfo.messagesBaseUrl)
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))

View File

@ -1,13 +1,21 @@
package io.github.wulkanowy.data
import android.content.Context
import android.os.Build
import androidx.javascriptengine.JavaScriptSandbox
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.google.common.util.concurrent.ListenableFuture
import dagger.hilt.android.qualifiers.ApplicationContext
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.data.repositories.WulkanowyRepository
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.scrapper.EvaluateHandler
import io.github.wulkanowy.utils.RemoteConfigHelper
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@ -16,18 +24,24 @@ import javax.inject.Singleton
@Singleton
class WulkanowySdkFactory @Inject constructor(
@ApplicationContext private val context: Context,
private val chuckerInterceptor: ChuckerInterceptor,
private val remoteConfig: RemoteConfigHelper,
private val webkitCookieManagerProxy: WebkitCookieManagerProxy,
private val studentDb: StudentDao,
private val wulkanowyRepository: WulkanowyRepository,
) {
private val eduOneMutex = Mutex()
private val migrationFailedStudentIds = mutableSetOf<Long>()
private val sandbox: ListenableFuture<JavaScriptSandbox>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && JavaScriptSandbox.isSupported())
JavaScriptSandbox.createConnectedInstanceAsync(context)
else null
private val sdk = Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL
androidVersion = Build.VERSION.RELEASE
buildTag = Build.MODEL
userAgentTemplate = remoteConfig.userAgentTemplate
setSimpleHttpLogger { Timber.d(it) }
setAdditionalCookieManager(webkitCookieManagerProxy)
@ -36,14 +50,46 @@ class WulkanowySdkFactory @Inject constructor(
addInterceptor(chuckerInterceptor, network = true)
}
fun create() = sdk
fun createBase() = sdk
suspend fun create(): Sdk {
val mapping = wulkanowyRepository.getMapping()
return createBase().apply {
if (mapping != null) {
endpointsMapping = mapping.endpoints
vTokenMapping = mapping.vTokens
vTokenSchemeMapping = mapping.vTokenScheme
vParamsEvaluation = createIsolate()
}
}
}
private suspend fun createIsolate(): suspend () -> EvaluateHandler {
return {
val isolate = sandbox?.await()?.createIsolate()
object : EvaluateHandler {
override suspend fun evaluate(code: String): String? {
return isolate?.evaluateJavaScriptAsync(code)?.await()
}
override fun close() {
isolate?.close()
}
}
}
}
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 {
private suspend fun buildSdk(
student: Student,
semester: Semester?,
isStudentEduOne: Boolean
): Sdk {
return create().apply {
email = student.email
password = student.password

View File

@ -0,0 +1,17 @@
package io.github.wulkanowy.data.api.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Mapping(
@SerialName("endpoints")
val endpoints: Map<String, Map<String, Map<String, String>>>,
@SerialName("vTokens")
val vTokens: Map<String, Map<String, Map<String, String>>>,
@SerialName("vTokenScheme")
val vTokenScheme: Map<String, Map<String, String>> = emptyMap(),
)

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.data.api
package io.github.wulkanowy.data.api.services
import io.github.wulkanowy.data.pojos.IntegrityRequest
import io.github.wulkanowy.data.pojos.LoginEvent

View File

@ -1,12 +1,16 @@
package io.github.wulkanowy.data.api
package io.github.wulkanowy.data.api.services
import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.db.entities.AdminMessage
import retrofit2.http.GET
import javax.inject.Singleton
@Singleton
interface AdminMessageService {
interface WulkanowyService {
@GET("/v1.json")
suspend fun getAdminMessages(): List<AdminMessage>
}
@GET("/mapping2.json")
suspend fun getMapping(): Mapping
}

View File

@ -9,6 +9,7 @@ import com.fredporciuncula.flow.preferences.Preference
import com.fredporciuncula.flow.preferences.Serializer
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.enums.AppTheme
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
import io.github.wulkanowy.data.enums.GradeColorTheme
@ -375,6 +376,15 @@ class PreferencesRepository @Inject constructor(
get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty()
private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) }
var mapping: Mapping?
get() {
val value = sharedPref.getString("mapping", null)
return value?.let { json.decodeFromString(it) }
}
set(value) = sharedPref.edit(commit = true) {
putString("mapping", value?.let { json.encodeToString(it) })
}
init {
if (installationId.isEmpty()) {
installationId = UUID.randomUUID().toString()

View File

@ -1,7 +1,7 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.api.services.SchoolsService
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters

View File

@ -1,20 +1,23 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.api.services.WulkanowyService
import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.networkBoundResource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.sync.Mutex
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AdminMessageRepository @Inject constructor(
private val adminMessageService: AdminMessageService,
class WulkanowyRepository @Inject constructor(
private val wulkanowyService: WulkanowyService,
private val adminMessageDao: AdminMessageDao,
private val preferencesRepository: PreferencesRepository,
) {
private val saveFetchResultMutex = Mutex()
@ -24,11 +27,28 @@ class AdminMessageRepository @Inject constructor(
mutex = saveFetchResultMutex,
isResultEmpty = { false },
query = { adminMessageDao.loadAll() },
fetch = { adminMessageService.getAdminMessages() },
fetch = { wulkanowyService.getAdminMessages() },
shouldFetch = { true },
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
},
)
.filterNot { it is Resource.Intermediate }
suspend fun getMapping(): Mapping? {
var savedMapping = preferencesRepository.mapping
if (savedMapping == null) {
fetchMapping()
savedMapping = preferencesRepository.mapping
}
return savedMapping
}
suspend fun fetchMapping() {
runCatching { wulkanowyService.getMapping() }
.onFailure { Timber.e(it) }
.onSuccess { preferencesRepository.mapping = it }
}
}

View File

@ -5,14 +5,14 @@ import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.repositories.AdminMessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetAppropriateAdminMessageUseCase @Inject constructor(
private val adminMessageRepository: AdminMessageRepository,
private val wulkanowyRepository: WulkanowyRepository,
private val preferencesRepository: PreferencesRepository,
private val appInfo: AppInfo
) {
@ -22,7 +22,7 @@ class GetAppropriateAdminMessageUseCase @Inject constructor(
}
operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow<Resource<AdminMessage?>> {
return adminMessageRepository.getAdminMessages().mapResourceData { adminMessages ->
return wulkanowyRepository.getAdminMessages().mapResourceData { adminMessages ->
adminMessages
.asSequence()
.filter { it.isNotDismissed() }

View File

@ -59,7 +59,7 @@ class GetMailboxByStudentUseCase @Inject constructor(
private fun String.getUnauthorizedVersion(): String {
return normalizeStudentName().split(" ")
.joinToString(" ") {
it.first() + "*".repeat(it.length - 1)
it.firstOrNull()?.toString().orEmpty() + "*".repeat((it.length - 1).coerceAtLeast(0))
}
}
}

View File

@ -59,7 +59,7 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
webView = this
with(settings) {
javaScriptEnabled = true
userAgentString = wulkanowySdkFactory.create().userAgent
userAgentString = wulkanowySdkFactory.createBase().userAgent
}
webViewClient = object : WebViewClient() {

View File

@ -118,5 +118,6 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
override fun onResume() {
super.onResume()
inAppUpdateHelper.onResume()
presenter.updateSdkMappings()
}
}

View File

@ -1,12 +1,15 @@
package io.github.wulkanowy.ui.modules.login
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
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 LoginPresenter @Inject constructor(
private val wulkanowyRepository: WulkanowyRepository,
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<LoginView>(errorHandler, studentRepository) {
@ -16,4 +19,11 @@ class LoginPresenter @Inject constructor(
view.initView()
Timber.i("Login view was initialized")
}
fun updateSdkMappings() {
presenterScope.launch {
runCatching { wulkanowyRepository.fetchMapping() }
.onFailure { Timber.e(it) }
}
}
}

View File

@ -138,6 +138,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
override fun onResume() {
super.onResume()
inAppUpdateHelper.onResume()
presenter.updateSdkMappings()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {

View File

@ -6,6 +6,7 @@ import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
@ -29,6 +30,7 @@ class MainPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val preferencesRepository: PreferencesRepository,
private val wulkanowyRepository: WulkanowyRepository,
private val syncManager: SyncManager,
private val analytics: AnalyticsHelper,
private val json: Json,
@ -199,4 +201,11 @@ class MainPresenter @Inject constructor(
.onFailure { errorHandler.dispatch(it) }
}
}
fun updateSdkMappings() {
presenterScope.launch {
runCatching { wulkanowyRepository.fetchMapping() }
.onFailure { Timber.e(it) }
}
}
}

View File

@ -1,8 +1,6 @@
Wersja 2.6.1
Wersja 2.6.8
dodaliśmy kalkulator frekwencji
dodaliśmy wyświetlanie lekcji dodatkowych w planie lekcji
— ulepszyliśmy wyjaśnienie na ekranie z miejscem na wpisanie numeru PESEL
— naprawiliśmy rzadkie sytuacje, gdy plan lekcji nakładał się na informację o jego braku
i znowu aktualizacja, żeby wiadomości działały chociaż w piątek. Później już pewnie nie będą
a i jeszcze naprawiliśmy dostęp mobilny, bo prosiliście
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -28,7 +28,7 @@
<string name="student_info_title">Student info</string>
<string name="dashboard_title">Dashboard</string>
<string name="notifications_center_title">Notifications center</string>
<string name="menu_order_title">Menu configuartion</string>
<string name="menu_order_title">Menu configuration</string>
<!--Subtitles-->

View File

@ -8,6 +8,7 @@ import io.mockk.mockk
fun createWulkanowySdkFactoryMock(sdk: Sdk) = mockk<WulkanowySdkFactory>()
.apply {
every { create() } returns sdk
every { createBase() } returns sdk
coEvery { create() } returns sdk
coEvery { create(any(), any()) } returns sdk
}

View File

@ -11,7 +11,6 @@ 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
@ -40,11 +39,12 @@ class WulkanowySdkFactoryTest {
chuckerInterceptor = mockk(),
remoteConfig = mockk(relaxed = true),
webkitCookieManagerProxy = mockk(),
studentDb = studentDao
studentDb = studentDao,
wulkanowyRepository = mockk(relaxed = true),
)
)
every { wulkanowySdkFactory.create() } returns sdk
coEvery { wulkanowySdkFactory.create() } returns sdk
}
@Test

View File

@ -3,6 +3,7 @@ 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.data.repositories.WulkanowyRepository
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AdsHelper
@ -31,6 +32,9 @@ class MainPresenterTest {
@MockK
lateinit var studentRepository: StudentRepository
@MockK(relaxed = true)
lateinit var wulkanowyRepository: WulkanowyRepository
@MockK(relaxed = true)
lateinit var prefRepository: PreferencesRepository
@ -65,7 +69,8 @@ class MainPresenterTest {
analytics = analytics,
json = Json,
appInfo = appInfo,
adsHelper = adsHelper
adsHelper = adsHelper,
wulkanowyRepository = wulkanowyRepository
)
presenter.onAttachView(mainView, null)
}