1
0

Compare commits

...

24 Commits

Author SHA1 Message Date
8390ccad20 Merge branch 'release/2.6.10' 2024-05-22 09:05:35 +02:00
bf9f048116 Version 2.6.10 2024-05-22 09:05:30 +02:00
56bfabdf11 Bump sdk to 2.6.9-SNAPSHOT 2024-05-22 08:14:11 +02:00
1a41e9e3ee Merge branch 'release/2.6.9' into develop 2024-05-17 21:23:40 +02:00
6e2bcfbe02 Merge branch 'release/2.6.9' 2024-05-17 21:23:34 +02:00
1c0df6c145 Version 2.6.9 2024-05-17 21:23:29 +02:00
2b61e883c5 Bump sdk to 2.6.8-SNAPSHOT 2024-05-17 19:45:53 +02:00
31a7ae6d15 Fix tests 2024-05-17 08:04:28 +02:00
4b3b4a21fa Merge branch 'release/2.6.8' into develop 2024-05-17 07:39:16 +02:00
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
21 changed files with 162 additions and 32 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 164
versionName "2.6.4"
versionCode 170
versionName "2.6.10"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
@ -161,7 +161,7 @@ play {
track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.99d
updatePriority = 4
updatePriority = 2
enabled.set(false)
}
@ -191,12 +191,13 @@ ext {
}
dependencies {
implementation 'io.github.wulkanowy:sdk:2.6.3'
implementation 'io.github.wulkanowy:sdk:2.6.9'
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 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,5 +1,5 @@
Wersja 2.6.4
Wersja 2.6.10
— naprawiliśmy dostęp do modułu ucznia i modułu wiadomości po kolejnej próbie blokady nas (niestety jeszcze bez nowego modułu ucznia)
— naprawiliśmy obsługę wiadomości (w środę rano, więc w kolejne dni może już serio nie działać)
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

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,13 @@ class WulkanowySdkFactoryTest {
chuckerInterceptor = mockk(),
remoteConfig = mockk(relaxed = true),
webkitCookieManagerProxy = mockk(),
studentDb = studentDao
studentDb = studentDao,
wulkanowyRepository = mockk(relaxed = true),
context = mockk(),
)
)
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)
}