Compare commits

...

64 Commits

Author SHA1 Message Date
85ac5769a1 [4.5.1-beta.1] Update build.gradle, signing and changelog. 2021-02-22 23:25:44 +01:00
93e3d5994a [Vulcan/Hebe] Restore sending date range when getting attendance. 2021-02-22 23:20:32 +01:00
0cf24c527b [Vulcan/Hebe] Fix logging tag in notices class. 2021-02-22 23:04:10 +01:00
97c5acd6ba [Vulcan/Hebe] Fix checking for current year. 2021-02-22 22:59:02 +01:00
30aeb70647 [Vulcan/Hebe] Add filtering data by current school year. 2021-02-22 22:54:13 +01:00
b599d679c4 [Vulcan/Hebe] Implement sending messages. 2021-02-22 22:43:36 +01:00
1eecd24d91 [Vulcan/Hebe] Add getting notices. 2021-02-22 22:40:28 +01:00
c698dfdb73 [Vulcan/Hebe] Add getting grade summary. 2021-02-22 21:33:42 +01:00
c7a44f5ced [Vulcan/Hebe] Implement push notifications. 2021-02-22 19:13:25 +01:00
c8ee6ff1e7 [Vulcan/Hebe] Add getting lucky number. 2021-02-22 18:49:53 +01:00
552acd4043 [Vulcan/Web] Fix web login. 2021-02-22 18:23:27 +01:00
ede101ea20 [Vulcan/Hebe] Add saving lesson topic in attendance. 2021-02-22 17:51:52 +01:00
13c2640ed5 [UI] Hide API deprecation message on other profiles. 2021-02-22 17:43:34 +01:00
dd0739fd4b Merge branch 'develop' into feature/vulcan-hebe 2021-02-22 17:36:48 +01:00
9023f13932 [Vulcan/Hebe] Add missing copyright to files. 2021-02-22 17:18:40 +01:00
f8456fb087 [Lab] Add archiver enabled checkbox. 2021-02-22 16:37:53 +01:00
9b48041cd9 [4.5] Update build.gradle, signing and changelog. 2021-02-22 00:03:52 +01:00
46cecf3474 Merge branch 'feature/vulcan-hebe' into develop 2021-02-21 23:57:25 +01:00
c27254bcad [Vulcan] Remove API login mode form. 2021-02-21 23:55:11 +01:00
80333cdea4 [Vulcan] Add API deprecation message. 2021-02-21 23:52:45 +01:00
6aee3ea420 [4.5-beta.2] Update build.gradle, signing and changelog. 2021-02-21 23:36:55 +01:00
a11a44b768 [Vulcan/Hebe] Add handling custom presence types. 2021-02-21 23:30:20 +01:00
e869107101 [Vulcan/Hebe] Add syncing both semesters during first sync. 2021-02-21 23:26:43 +01:00
5903bbe59d [Vulcan/Hebe] Fix getting attendance. 2021-02-21 23:03:06 +01:00
6c0ddd3e6d [Vulcan/Hebe] Add setting message status as read. 2021-02-21 22:55:26 +01:00
621a7ac642 [Vulcan/Hebe] Add getting attendance. 2021-02-21 22:28:54 +01:00
e86b47fb1b [Vulcan/Hebe] Add getting messages. 2021-02-21 21:50:44 +01:00
98fb7ac8c9 [Vulcan/Web] Fix Proguard rules for platform selection. 2021-02-21 19:15:53 +01:00
f49e39e858 [Vulcan/Hebe] Add getting teacher list and addressbook. 2021-02-21 19:13:47 +01:00
8fc57cd3f5 [Vulcan/Hebe] Fix getting classroom name. 2021-02-21 17:22:21 +01:00
a9eda087e0 [Extensions] Update Gson extensions to check type. 2021-02-21 17:20:13 +01:00
3f36a284ee [4.5-beta.1] Update build.gradle, signing and changelog. 2021-02-21 16:40:40 +01:00
1814fd67e1 [Strings] Update copyright date. 2021-02-21 16:39:02 +01:00
54e49af943 [Vulcan/Hebe] Add getting timetable and lesson changes. 2021-02-21 16:18:33 +01:00
d6a67a0da6 [Vulcan/Hebe] Add getting homework list. 2021-02-21 12:18:53 +01:00
28725c6400 [Vulcan/Hebe] Add getting exams. 2021-02-21 00:27:42 +01:00
4fc965d970 [Vulcan/Hebe] Add saving unit ID. 2021-02-20 23:52:40 +01:00
2aaf713d58 [Vulcan/Hebe] Add next sync and data removing in grades. 2021-02-20 23:45:27 +01:00
c7d2ac4e3e [Vulcan/Hebe] Add getting grades. 2021-02-20 23:07:23 +01:00
ae20c30c88 [Vulcan/Hebe] Fix Firebase token for registration. 2021-02-20 21:31:52 +01:00
aef3f66654 [Vulcan/Hebe] Add API list helper. Separate student list code. 2021-02-20 20:11:54 +01:00
c7abde8f11 [Vulcan/Hebe] Update login mode requirement. 2021-02-20 18:51:08 +01:00
2fcff33bd6 [Vulcan/Hebe] Add hebe API login implementation. 2021-02-19 13:37:31 +01:00
b08e4c2d3d [Gradle] Update OkHttp to 3.12.13. 2021-02-19 13:24:12 +01:00
73ff09052c [Gradle] Bump target SDK to 30. 2021-02-17 14:43:00 +01:00
9649afd43f [Gradle] Update libraries and dependencies. 2021-02-17 14:42:08 +01:00
ed3a245b51 [UI/Login] Add new easter eggs. 2020-10-18 22:14:41 +02:00
477730708f [UI/Login] Add refresh button in platform list. 2020-10-17 23:44:17 +02:00
f39d0c595d [UI/Login] Add recommended, testing and dev only badges to login modes. 2020-10-17 23:10:07 +02:00
46407f9647 [4.4.3] Update build.gradle, signing and changelog. 2020-10-16 23:54:48 +02:00
6ecb97b87e [Login/Podlasie] Add option to logout other devices. 2020-10-16 23:50:44 +02:00
ecdaaeae65 [API/Vulcan] Add KO1 routing rule. 2020-10-16 18:06:34 +02:00
a0c302b663 [App] Swap devMode with debugMode. Fix hiding sticks from old. 2020-10-16 17:18:19 +02:00
b31039ecd9 [API/Mobidziennik] Fix getting recipient list. 2020-10-16 16:57:00 +02:00
5c84086f42 [Settings/Grades] Add hiding sticks from old. 2020-10-14 22:26:47 +02:00
752cdfa8d6 Implement wear module base. 2020-09-17 18:05:17 +02:00
8e3d404352 [4.4.2] Update build.gradle, signing and changelog. 2020-09-05 19:14:11 +02:00
810cfd8092 [API] Rename response parameters to fix compatibility. 2020-09-05 19:13:37 +02:00
bd2a9524c6 [UI] Use HtmlCompat instead of Html. Fix a typo. 2020-09-05 18:47:30 +02:00
d780d5118d [Proguard] Add rules to fix API responses. 2020-09-05 18:38:53 +02:00
f1570b8eb9 [4.4.1] Update build.gradle, signing and changelog. 2020-09-04 15:44:26 +02:00
de0f29a09e [API/Mobidziennik] Fix getting attendance when a day has no lessons. 2020-09-04 00:04:23 +02:00
c0d11c91e3 [API/Mobidziennik] Fix trimming subject name in timetable. 2020-09-03 23:51:56 +02:00
22c540a3d4 [UI] Improve register unavailable dialog and card. 2020-09-03 23:50:53 +02:00
128 changed files with 3947 additions and 674 deletions

View File

@ -1,6 +1,10 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
@ -11,7 +15,6 @@
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -22,7 +25,6 @@
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -34,7 +36,6 @@
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@ -45,7 +46,6 @@
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@ -56,7 +56,6 @@
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -67,7 +66,6 @@
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -78,7 +76,6 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -90,7 +87,6 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@ -102,7 +98,6 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
@ -112,5 +107,8 @@
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

15
.idea/compiler.xml generated
View File

@ -4,13 +4,14 @@
<bytecodeTargetLevel target="1.7">
<module name="annotation" target="1.7" />
<module name="codegen" target="1.7" />
<module name="Szkolny.eu.agendacalendarview" target="1.8" />
<module name="Szkolny.eu.app" target="1.8" />
<module name="Szkolny.eu.cafebar" target="1.8" />
<module name="Szkolny.eu.material-about-library" target="1.8" />
<module name="Szkolny.eu.mhttp" target="1.8" />
<module name="Szkolny.eu.nachos" target="1.8" />
<module name="Szkolny.eu.szkolny-font" target="1.8" />
<module name="Szkolny.eu.agendacalendarview" target="11" />
<module name="Szkolny.eu.app" target="11" />
<module name="Szkolny.eu.cafebar" target="11" />
<module name="Szkolny.eu.material-about-library" target="11" />
<module name="Szkolny.eu.mhttp" target="11" />
<module name="Szkolny.eu.nachos" target="11" />
<module name="Szkolny.eu.szkolny-font" target="11" />
<module name="Szkolny.eu.wear" target="11" />
</bytecodeTargetLevel>
</component>
</project>

2
.idea/discord.xml generated
View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="true" />
<option name="show" value="PROJECT_FILES" />
</component>
<component name="ProjectNotificationSettings">
<option name="askShowProject" value="false" />

View File

@ -3,6 +3,7 @@
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />

View File

@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'io.fabric'
apply plugin: 'com.google.firebase.crashlytics'
android {
signingConfigs {
@ -54,10 +54,11 @@ android {
lintOptions {
checkReleaseBuilds false
}
dataBinding {
enabled = true
buildFeatures {
dataBinding = true
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility '1.8'
targetCompatibility '1.8'
}
@ -75,6 +76,7 @@ android {
version "3.10.2"
}
}
ndkVersion '21.3.6528147'
}
/*task finalizeBundleDebug(type: Copy) {
@ -104,6 +106,8 @@ tasks.whenTaskAdded { task ->
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
kapt "androidx.room:room-compiler:${versions.room}"
debugImplementation "com.amitshekhar.android:debug-db:1.0.5"
@ -114,7 +118,7 @@ dependencies {
implementation "androidx.core:core-ktx:${versions.ktx}"
implementation "androidx.gridlayout:gridlayout:${versions.gridLayout}"
implementation "androidx.legacy:legacy-support-v4:${versions.legacy}"
implementation "androidx.lifecycle:lifecycle-livedata:${versions.lifecycle}"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${versions.lifecycle}"
implementation "androidx.recyclerview:recyclerview:${versions.recyclerView}"
implementation "androidx.room:room-runtime:${versions.room}"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}"
@ -137,7 +141,7 @@ dependencies {
implementation "cat.ereza:customactivityoncrash:2.2.0"
implementation "com.applandeo:material-calendar-view:1.5.0"
implementation "com.crashlytics.sdk.android:crashlytics:2.10.1"
implementation 'com.google.firebase:firebase-crashlytics:17.3.1'
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.evernote:android-job:1.2.6"
implementation "com.github.antonKozyriatskyi:CircularProgressIndicator:1.2.2"
@ -147,7 +151,7 @@ dependencies {
implementation "com.jaredrummler:colorpicker:1.0.2"
implementation("com.squareup.okhttp3:okhttp") {
version {
strictly "3.12.2"
strictly "3.12.13"
}
}
implementation "com.theartofdev.edmodo:android-image-cropper:2.8.0" // do not update
@ -157,7 +161,7 @@ dependencies {
implementation "me.grantland:autofittextview:0.2.1"
implementation "me.leolin:ShortcutBadger:1.1.22@aar"
implementation "org.greenrobot:eventbus:3.1.1"
implementation "org.jsoup:jsoup:1.10.1"
implementation "org.jsoup:jsoup:1.12.1"
implementation "pl.droidsonroids.gif:android-gif-drawable:1.2.15"
//implementation "se.emilsjolander:stickylistheaders:2.7.0"
implementation 'com.github.edisonw:StickyListHeaders:master-SNAPSHOT@aar'
@ -180,6 +184,7 @@ dependencies {
//implementation "org.redundent:kotlin-xml-builder:1.5.3"
implementation "io.github.wulkanowy:signer-android:0.1.1"
implementation 'com.github.wulkanowy.uonet-request-signer:hebe-jvm:a99ca50a31'
implementation "androidx.work:work-runtime-ktx:${versions.work}"

View File

@ -64,6 +64,6 @@
-keep class pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing { public final byte[] pleaseStopRightNow(java.lang.String, long); }
-keepclassmembernames class pl.szczodrzynski.edziennik.data.api.szkolny.request.** { *; }
-keepclassmembernames class pl.szczodrzynski.edziennik.data.api.szkolny.response.** { *; }
-keepclassmembernames class pl.szczodrzynski.edziennik.ui.modules.login.LoginInfo.Platform { *; }
-keepclassmembers class pl.szczodrzynski.edziennik.data.api.szkolny.request.** { *; }
-keepclassmembers class pl.szczodrzynski.edziennik.data.api.szkolny.response.** { *; }
-keepclassmembernames class pl.szczodrzynski.edziennik.ui.modules.login.LoginInfo$Platform { *; }

View File

@ -1,8 +1,10 @@
<h3>Wersja 4.4, 2020-08-27</h3>
<h3>Wersja 4.5.1-beta.1, 2021-02-22</h3>
<ul>
<li>Poprawione komunikaty o aktualizacjach aplikacji.</li>
<li>Vulcan: dodano wyświetlanie <b>szczęśliwego numerka.</b></li>
<li>Vulcan: przywrócono uwagi oraz wysyłanie wiadomości.</li>
<li>Vulcan: usunięto wyświetlanie zadań domowych i sprawdzianów z poprzednich lat.</li>
</ul>
<br>
<br>
Dzięki za korzystanie ze Szkolnego!<br>
<i>&copy; Kuba Szczodrzyński, Kacper Ziubryniewicz 2020</i>
<i>&copy; Kuba Szczodrzyński, Kacper Ziubryniewicz 2021</i>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/
static toys AES_IV[16] = {
0x85, 0xd2, 0x0d, 0x9e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
0x42, 0xe5, 0x9a, 0x4e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

View File

@ -57,8 +57,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val profileId
get() = profile.id
var devMode = false
var debugMode = false
var devMode = false
}
val notificationChannelsManager by lazy { NotificationChannelsManager(this) }
@ -107,7 +107,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
.readTimeout(30, TimeUnit.SECONDS)
builder.installHttpsSupport(this)
if (debugMode || BuildConfig.DEBUG) {
if (devMode || BuildConfig.DEBUG) {
HyperLog.initialize(this)
HyperLog.setLogLevel(Log.VERBOSE)
HyperLog.setLogFormat(DebugLogFormat(this))
@ -162,7 +162,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
Iconics.registerFont(SzkolnyFont)
App.db = AppDb(this)
Themes.themeInt = config.ui.theme
debugMode = config.debugMode
devMode = config.debugMode
MHttp.instance().customOkHttpClient(http)
if (!profileLoadById(config.lastProfileId)) {
@ -173,9 +173,9 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
setLanguage(it)
}
devMode = BuildConfig.DEBUG
debugMode = BuildConfig.DEBUG
if (BuildConfig.DEBUG)
debugMode = true
devMode = true
Signing.getCert(this)
@ -185,7 +185,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
if (config.devModePassword != null)
checkDevModePassword()
debugMode = devMode || config.debugMode
devMode = debugMode || config.debugMode
if (config.sync.enabled)
SyncWorker.scheduleNext(this@App, false)
@ -294,6 +294,19 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
"Vulcan"
)
val pushVulcanHebeApp = FirebaseApp.initializeApp(
this@App,
FirebaseOptions.Builder()
.setProjectId("dzienniczekplus")
.setStorageBucket("dzienniczekplus.appspot.com")
.setDatabaseUrl("https://dzienniczekplus.firebaseio.com")
.setGcmSenderId("987828170337")
.setApiKey("AIzaSyDW8MUtanHy64_I0oCpY6cOxB3jrvJd_iA")
.setApplicationId("1:987828170337:android:7e16404b9e5deaaa")
.build(),
"VulcanHebe"
)
try {
FirebaseInstanceId.getInstance().instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
@ -324,6 +337,14 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
config.sync.tokenVulcanList = listOf()
}
}
FirebaseInstanceId.getInstance(pushVulcanHebeApp).instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
d("Firebase", "Got VulcanHebe token: $token")
if (token != config.sync.tokenVulcanHebe) {
config.sync.tokenVulcanHebe = token
config.sync.tokenVulcanHebeList = listOf()
}
}
FirebaseMessaging.getInstance().subscribeToTopic(packageName)
} catch (e: IllegalStateException) {
e.printStackTrace()

View File

@ -96,30 +96,30 @@ fun List<Teacher>.byNameFDotSpaceLast(nameFDotSpaceLast: String) = firstOrNull {
fun JsonObject?.get(key: String): JsonElement? = this?.get(key)
fun JsonObject?.getBoolean(key: String): Boolean? = get(key)?.let { if (it.isJsonNull) null else it.asBoolean }
fun JsonObject?.getString(key: String): String? = get(key)?.let { if (it.isJsonNull) null else it.asString }
fun JsonObject?.getInt(key: String): Int? = get(key)?.let { if (it.isJsonNull) null else it.asInt }
fun JsonObject?.getLong(key: String): Long? = get(key)?.let { if (it.isJsonNull) null else it.asLong }
fun JsonObject?.getFloat(key: String): Float? = get(key)?.let { if(it.isJsonNull) null else it.asFloat }
fun JsonObject?.getChar(key: String): Char? = get(key)?.let { if(it.isJsonNull) null else it.asCharacter }
fun JsonObject?.getBoolean(key: String): Boolean? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asBoolean }
fun JsonObject?.getString(key: String): String? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asString }
fun JsonObject?.getInt(key: String): Int? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asInt }
fun JsonObject?.getLong(key: String): Long? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asLong }
fun JsonObject?.getFloat(key: String): Float? = get(key)?.let { if(!it.isJsonPrimitive) null else it.asFloat }
fun JsonObject?.getChar(key: String): Char? = get(key)?.let { if(!it.isJsonPrimitive) null else it.asCharacter }
fun JsonObject?.getJsonObject(key: String): JsonObject? = get(key)?.let { if (it.isJsonObject) it.asJsonObject else null }
fun JsonObject?.getJsonArray(key: String): JsonArray? = get(key)?.let { if (it.isJsonArray) it.asJsonArray else null }
fun JsonObject?.getBoolean(key: String, defaultValue: Boolean): Boolean = get(key)?.let { if (it.isJsonNull) defaultValue else it.asBoolean } ?: defaultValue
fun JsonObject?.getString(key: String, defaultValue: String): String = get(key)?.let { if (it.isJsonNull) defaultValue else it.asString } ?: defaultValue
fun JsonObject?.getInt(key: String, defaultValue: Int): Int = get(key)?.let { if (it.isJsonNull) defaultValue else it.asInt } ?: defaultValue
fun JsonObject?.getLong(key: String, defaultValue: Long): Long = get(key)?.let { if (it.isJsonNull) defaultValue else it.asLong } ?: defaultValue
fun JsonObject?.getFloat(key: String, defaultValue: Float): Float = get(key)?.let { if(it.isJsonNull) defaultValue else it.asFloat } ?: defaultValue
fun JsonObject?.getChar(key: String, defaultValue: Char): Char = get(key)?.let { if(it.isJsonNull) defaultValue else it.asCharacter } ?: defaultValue
fun JsonObject?.getBoolean(key: String, defaultValue: Boolean): Boolean = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asBoolean } ?: defaultValue
fun JsonObject?.getString(key: String, defaultValue: String): String = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asString } ?: defaultValue
fun JsonObject?.getInt(key: String, defaultValue: Int): Int = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asInt } ?: defaultValue
fun JsonObject?.getLong(key: String, defaultValue: Long): Long = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asLong } ?: defaultValue
fun JsonObject?.getFloat(key: String, defaultValue: Float): Float = get(key)?.let { if(!it.isJsonPrimitive) defaultValue else it.asFloat } ?: defaultValue
fun JsonObject?.getChar(key: String, defaultValue: Char): Char = get(key)?.let { if(!it.isJsonPrimitive) defaultValue else it.asCharacter } ?: defaultValue
fun JsonObject?.getJsonObject(key: String, defaultValue: JsonObject): JsonObject = get(key)?.let { if (it.isJsonObject) it.asJsonObject else defaultValue } ?: defaultValue
fun JsonObject?.getJsonArray(key: String, defaultValue: JsonArray): JsonArray = get(key)?.let { if (it.isJsonArray) it.asJsonArray else defaultValue } ?: defaultValue
fun JsonArray.getBoolean(key: Int): Boolean? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asBoolean }
fun JsonArray.getString(key: Int): String? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asString }
fun JsonArray.getInt(key: Int): Int? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asInt }
fun JsonArray.getLong(key: Int): Long? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asLong }
fun JsonArray.getFloat(key: Int): Float? = if (key >= size()) null else get(key)?.let { if(it.isJsonNull) null else it.asFloat }
fun JsonArray.getChar(key: Int): Char? = if (key >= size()) null else get(key)?.let { if(it.isJsonNull) null else it.asCharacter }
fun JsonArray.getBoolean(key: Int): Boolean? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asBoolean }
fun JsonArray.getString(key: Int): String? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asString }
fun JsonArray.getInt(key: Int): Int? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asInt }
fun JsonArray.getLong(key: Int): Long? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asLong }
fun JsonArray.getFloat(key: Int): Float? = if (key >= size()) null else get(key)?.let { if(!it.isJsonPrimitive) null else it.asFloat }
fun JsonArray.getChar(key: Int): Char? = if (key >= size()) null else get(key)?.let { if(!it.isJsonPrimitive) null else it.asCharacter }
fun JsonArray.getJsonObject(key: Int): JsonObject? = if (key >= size()) null else get(key)?.let { if (it.isJsonObject) it.asJsonObject else null }
fun JsonArray.getJsonArray(key: Int): JsonArray? = if (key >= size()) null else get(key)?.let { if (it.isJsonArray) it.asJsonArray else null }
@ -299,7 +299,7 @@ fun colorFromCssName(name: String): Int {
"orange" -> 0xffffa500
"black" -> 0xff000000
"white" -> 0xffffffff
else -> -1
else -> -1L
}.toInt()
}
@ -538,6 +538,12 @@ fun String.md5(): String {
return BigInteger(1, md.digest(toByteArray())).toString(16).padStart(32, '0')
}
fun String.sha1Hex(): String {
val md = MessageDigest.getInstance("SHA-1")
md.update(toByteArray())
return md.digest().joinToString("") { "%02x".format(it) }
}
fun String.sha256(): ByteArray {
val md = MessageDigest.getInstance("SHA-256")
md.update(toByteArray())
@ -1172,7 +1178,7 @@ fun Iterable<Float>.averageOrNull() = this.average().let { if (it.isNaN()) null
fun String.copyToClipboard(context: Context) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText("Tekst", this)
clipboard.primaryClip = clipData
clipboard.setPrimaryClip(clipData)
}
fun TextView.getTextPosition(range: IntRange): Rect {

View File

@ -38,6 +38,7 @@ import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.droidsonroids.gif.GifDrawable
import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_API_DEPRECATED
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.*
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -64,6 +65,7 @@ import pl.szczodrzynski.edziennik.ui.modules.base.MainSnackbar
import pl.szczodrzynski.edziennik.ui.modules.behaviour.BehaviourFragment
import pl.szczodrzynski.edziennik.ui.modules.debug.DebugFragment
import pl.szczodrzynski.edziennik.ui.modules.debug.LabFragment
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorDetailsDialog
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar
import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackFragment
import pl.szczodrzynski.edziennik.ui.modules.feedback.HelpFragment
@ -232,7 +234,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
list += NavTarget(TARGET_MESSAGES_DETAILS, R.string.menu_message, MessageFragment::class).withPopTo(DRAWER_ITEM_MESSAGES)
list += NavTarget(TARGET_MESSAGES_COMPOSE, R.string.menu_message_compose, MessagesComposeFragment::class)
list += NavTarget(TARGET_WEB_PUSH, R.string.menu_web_push, WebPushFragment::class)
if (App.debugMode) {
if (App.devMode) {
list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class)
list += NavTarget(TARGET_LAB, R.string.menu_lab, LabFragment::class)
.withIcon(CommunityMaterial.Icon.cmd_flask_outline)
@ -566,7 +568,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
.withIcon(CommunityMaterial.Icon2.cmd_help_circle_outline)
.withOnClickListener(View.OnClickListener { loadTarget(TARGET_FEEDBACK) })
)
if (App.debugMode) {
if (App.devMode) {
bottomSheet += BottomSheetPrimaryItem(false)
.withTitle(R.string.menu_debug)
.withIcon(CommunityMaterial.Icon.cmd_android_studio)
@ -647,7 +649,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
app.profile.registerName?.let { registerName ->
var status = app.config.sync.registerAvailability[registerName]
if (status == null || status.nextCheck < currentTimeUnix()) {
if (status == null || status.nextCheckAt < currentTimeUnix()) {
withContext(Dispatchers.IO) {
val api = SzkolnyApi(app)
api.runCatching(this@MainActivity) {
@ -749,6 +751,11 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onApiTaskErrorEvent(event: ApiTaskErrorEvent) {
EventBus.getDefault().removeStickyEvent(event)
if (event.error.errorCode == ERROR_VULCAN_API_DEPRECATED) {
if (event.error.profileId != App.profileId)
return
ErrorDetailsDialog(this, listOf(event.error))
}
navView.toolbar.apply {
subtitleFormat = R.string.toolbar_subtitle
subtitleFormatWithUnread = R.plurals.toolbar_subtitle_with_unread

View File

@ -105,6 +105,11 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
get() { mWidgetConfigs = mWidgetConfigs ?: values.get("widgetConfigs", JsonObject()); return mWidgetConfigs ?: JsonObject() }
set(value) { set("widgetConfigs", value); mWidgetConfigs = value }
private var mArchiverEnabled: Boolean? = null
var archiverEnabled: Boolean
get() { mArchiverEnabled = mArchiverEnabled ?: values.get("archiverEnabled", true); return mArchiverEnabled ?: true }
set(value) { set("archiverEnabled", value); mArchiverEnabled = value }
private var rawEntries: List<ConfigEntry> = db.configDao().getAllNow()
private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf()
init {

View File

@ -99,6 +99,10 @@ class ConfigSync(private val config: Config) {
var tokenVulcan: String?
get() { mTokenVulcan = mTokenVulcan ?: config.values.get("tokenVulcan", null as String?); return mTokenVulcan }
set(value) { config.set("tokenVulcan", value); mTokenVulcan = value }
private var mTokenVulcanHebe: String? = null
var tokenVulcanHebe: String?
get() { mTokenVulcanHebe = mTokenVulcanHebe ?: config.values.get("tokenVulcanHebe", null as String?); return mTokenVulcanHebe }
set(value) { config.set("tokenVulcanHebe", value); mTokenVulcanHebe = value }
private var mTokenMobidziennikList: List<Int>? = null
var tokenMobidziennikList: List<Int>
@ -112,6 +116,10 @@ class ConfigSync(private val config: Config) {
var tokenVulcanList: List<Int>
get() { mTokenVulcanList = mTokenVulcanList ?: config.values.getIntList("tokenVulcanList", listOf()); return mTokenVulcanList ?: listOf() }
set(value) { config.set("tokenVulcanList", value); mTokenVulcanList = value }
private var mTokenVulcanHebeList: List<Int>? = null
var tokenVulcanHebeList: List<Int>
get() { mTokenVulcanHebeList = mTokenVulcanHebeList ?: config.values.getIntList("tokenVulcanHebeList", listOf()); return mTokenVulcanHebeList ?: listOf() }
set(value) { config.set("tokenVulcanHebeList", value); mTokenVulcanHebeList = value }
private var mRegisterAvailability: Map<String, RegisterAvailabilityStatus>? = null
var registerAvailability: Map<String, RegisterAvailabilityStatus>

View File

@ -49,4 +49,9 @@ class ProfileConfigGrades(private val config: ProfileConfig) {
var dontCountGrades: List<String>
get() { mDontCountGrades = mDontCountGrades ?: config.values.get("dontCountGrades", listOf()); return mDontCountGrades ?: listOf() }
set(value) { config.set("dontCountGrades", value); mDontCountGrades = value }
private var mHideSticksFromOld: Boolean? = null
var hideSticksFromOld: Boolean
get() { mHideSticksFromOld = mHideSticksFromOld ?: config.values.get("hideSticksFromOld", false); return mHideSticksFromOld ?: false }
set(value) { config.set("hideSticksFromOld", value); mHideSticksFromOld = value }
}

View File

@ -95,7 +95,16 @@ const val VULCAN_API_APP_NAME = "VULCAN-Android-ModulUcznia"
const val VULCAN_API_APP_VERSION = "20.5.1.470"
const val VULCAN_API_PASSWORD = "CE75EA598C7743AD9B0B7328DED85B06"
const val VULCAN_API_PASSWORD_FAKELOG = "012345678901234567890123456789AB"
val VULCAN_API_DEVICE_NAME = "Szkolny.eu ${Build.MODEL}"
const val VULCAN_HEBE_USER_AGENT = "Dart/2.10 (dart:io)"
const val VULCAN_HEBE_APP_NAME = "DzienniczekPlus 2.0"
const val VULCAN_HEBE_APP_VERSION = "21.02.09 (G)"
private const val VULCAN_API_DEVICE_NAME_PREFIX = "Szkolny.eu "
private const val VULCAN_API_DEVICE_NAME_SUFFIX = " - nie usuwać"
val VULCAN_API_DEVICE_NAME by lazy {
val base = "$VULCAN_API_DEVICE_NAME_PREFIX${Build.MODEL}"
val baseMaxLength = 50 - VULCAN_API_DEVICE_NAME_SUFFIX.length
base.take(baseMaxLength) + VULCAN_API_DEVICE_NAME_SUFFIX
}
const val VULCAN_API_ENDPOINT_CERTIFICATE = "mobile-api/Uczen.v3.UczenStart/Certyfikat"
const val VULCAN_API_ENDPOINT_STUDENT_LIST = "mobile-api/Uczen.v3.UczenStart/ListaUczniow"
@ -116,9 +125,26 @@ const val VULCAN_API_ENDPOINT_MESSAGES_ATTACHMENTS = "mobile-api/Uczen.v3.Uczen/
const val VULCAN_API_ENDPOINT_HOMEWORK_ATTACHMENTS = "mobile-api/Uczen.v3.Uczen/ZadaniaDomoweZalacznik"
const val VULCAN_WEB_ENDPOINT_LUCKY_NUMBER = "Start.mvc/GetKidsLuckyNumbers"
const val VULCAN_WEB_ENDPOINT_REGISTER_DEVICE = "RejestracjaUrzadzeniaToken.mvc/Get"
const val VULCAN_HEBE_ENDPOINT_REGISTER_NEW = "api/mobile/register/new"
const val VULCAN_HEBE_ENDPOINT_MAIN = "api/mobile/register/hebe"
const val VULCAN_HEBE_ENDPOINT_PUSH_ALL = "api/mobile/push/all"
const val VULCAN_HEBE_ENDPOINT_TIMETABLE = "api/mobile/schedule"
const val VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGES = "api/mobile/schedule/changes"
const val VULCAN_HEBE_ENDPOINT_ADDRESSBOOK = "api/mobile/addressbook"
const val VULCAN_HEBE_ENDPOINT_EXAMS = "api/mobile/exam"
const val VULCAN_HEBE_ENDPOINT_GRADES = "api/mobile/grade"
const val VULCAN_HEBE_ENDPOINT_GRADE_SUMMARY = "api/mobile/grade/summary"
const val VULCAN_HEBE_ENDPOINT_HOMEWORK = "api/mobile/homework"
const val VULCAN_HEBE_ENDPOINT_NOTICES = "api/mobile/note"
const val VULCAN_HEBE_ENDPOINT_ATTENDANCE = "api/mobile/lesson"
const val VULCAN_HEBE_ENDPOINT_MESSAGES = "api/mobile/message"
const val VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS = "api/mobile/message/status"
const val VULCAN_HEBE_ENDPOINT_MESSAGES_SEND = "api/mobile/message"
const val VULCAN_HEBE_ENDPOINT_LUCKY_NUMBER = "api/mobile/school/lucky"
const val EDUDZIENNIK_USER_AGENT = "Szkolny.eu/${BuildConfig.VERSION_NAME}"
const val PODLASIE_API_VERSION = "1.0.31"
const val PODLASIE_API_VERSION = "1.0.62"
const val PODLASIE_API_URL = "https://cpdklaser.zeto.bialystok.pl/api"
const val PODLASIE_API_USER_ENDPOINT = "/pobierzDaneUcznia"
const val PODLASIE_API_LOGOUT_DEVICES_ENDPOINT = "/wyczyscUrzadzenia"

View File

@ -170,6 +170,8 @@ const val ERROR_VULCAN_WEB_LOGGED_OUT = 350
const val ERROR_VULCAN_WEB_CERTIFICATE_POST_FAILED = 351
const val ERROR_VULCAN_WEB_GRADUATE_ACCOUNT = 352
const val ERROR_VULCAN_WEB_NO_SCHOOLS = 353
const val ERROR_VULCAN_HEBE_OTHER = 354
const val ERROR_VULCAN_API_DEPRECATED = 390
const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN = 401
const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME = 402
@ -229,5 +231,6 @@ const val ERROR_ONEDRIVE_DOWNLOAD = 930
const val EXCEPTION_VULCAN_WEB_LOGIN = 931
const val EXCEPTION_VULCAN_WEB_REQUEST = 932
const val EXCEPTION_PODLASIE_API_REQUEST = 940
const val EXCEPTION_VULCAN_HEBE_REQUEST = 950
const val LOGIN_NO_ARGUMENTS = 1201

View File

@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLogi
import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginWeb
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginHebe
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain
import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
@ -98,11 +99,13 @@ val mobidziennikLoginMethods = listOf(
const val LOGIN_TYPE_VULCAN = 4
const val LOGIN_MODE_VULCAN_API = 0
const val LOGIN_MODE_VULCAN_WEB = 1
const val LOGIN_MODE_VULCAN_HEBE = 2
const val LOGIN_METHOD_VULCAN_WEB_MAIN = 100
const val LOGIN_METHOD_VULCAN_WEB_NEW = 200
const val LOGIN_METHOD_VULCAN_WEB_OLD = 300
const val LOGIN_METHOD_VULCAN_WEB_MESSAGES = 400
const val LOGIN_METHOD_VULCAN_API = 500
const val LOGIN_METHOD_VULCAN_HEBE = 600
val vulcanLoginMethods = listOf(
LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_MAIN, VulcanLoginWebMain::class.java)
.withIsPossible { _, loginStore -> loginStore.hasLoginData("webHost") }
@ -117,9 +120,17 @@ val vulcanLoginMethods = listOf(
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_VULCAN_WEB_MAIN },*/
LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_API, VulcanLoginApi::class.java)
.withIsPossible { _, _ -> true }
.withIsPossible { _, loginStore ->
loginStore.mode == LOGIN_MODE_VULCAN_API
}
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED },
LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_HEBE, VulcanLoginHebe::class.java)
.withIsPossible { _, loginStore ->
loginStore.mode != LOGIN_MODE_VULCAN_API
}
.withRequiredLoginMethod { _, loginStore ->
if (loginStore.mode == LOGIN_MODE_VULCAN_WEB) LOGIN_METHOD_VULCAN_WEB_MAIN else LOGIN_METHOD_NOT_NEEDED
if (loginStore.mode == LOGIN_MODE_VULCAN_WEB) LOGIN_METHOD_VULCAN_WEB_MAIN else LOGIN_METHOD_NOT_NEEDED
}
)

View File

@ -12,6 +12,10 @@ object Regexes {
"""color: (\w+);?""".toRegex()
}
val NOT_DIGITS by lazy {
"""[^0-9]""".toRegex()
}
val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy {
@ -80,6 +84,15 @@ object Regexes {
val MOBIDZIENNIK_ATTENDANCE_ENTRIES by lazy {
"""font-size:.+?class=".*?">(.*?)</td>""".toRegex(DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_ATTENDANCE_COLUMNS by lazy {
"""<tr><td class="border-right1".+?/td>(.+?)</tr>""".toRegex(DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_ATTENDANCE_COLUMN by lazy {
"""(<td.+?>)(.*?)</td>""".toRegex(DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_ATTENDANCE_COLUMN_SPAN by lazy {
"""colspan="(\d+)"""".toRegex()
}
val MOBIDZIENNIK_ATTENDANCE_RANGE by lazy {
"""<span>([0-9:]+) - .+? (.+?)</span></a>""".toRegex(DOT_MATCHES_ALL)
}

View File

@ -93,7 +93,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
profile.registerName?.let { registerName ->
var status = app.config.sync.registerAvailability[registerName]
if (status == null || status.nextCheck < currentTimeUnix()) {
if (status == null || status.nextCheckAt < currentTimeUnix()) {
val api = SzkolnyApi(app)
api.runCatching({
val availability = getRegisterAvailability()

View File

@ -44,7 +44,7 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
dataDays.remove(date.value)
val subjectId = data.subjectList.singleOrNull { it.longName == lesson[5] }?.id ?: -1
val subjectId = data.subjectList.singleOrNull { it.longName == lesson[5].trim() }?.id ?: -1
val teacherId = data.teacherList.singleOrNull { it.fullNameLastFirst == (lesson[7]+" "+lesson[6]).fixName() }?.id ?: -1
val teamId = data.teamList.singleOrNull { it.name == lesson[8]+lesson[9] }?.id ?: -1
val classroom = lesson[11]

View File

@ -91,8 +91,11 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik,
Regexes.MOBIDZIENNIK_ATTENDANCE_TABLE.findAll(text).forEach { tableResult ->
val table = tableResult[1]
val lessonDates = mutableListOf<Date>()
val entries = mutableListOf<String>()
val ranges = mutableListOf<MatchResult?>()
Regexes.MOBIDZIENNIK_ATTENDANCE_LESSON_COUNT.findAll(table).forEach {
val date = Date.fromY_m_d(it[1])
for (i in 0 until (it[2].toIntOrNull() ?: 0)) {
@ -101,102 +104,52 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik,
}
Regexes.MOBIDZIENNIK_ATTENDANCE_ENTRIES.findAll(table).mapTo(entries) { it[1] }
Regexes.MOBIDZIENNIK_ATTENDANCE_COLUMNS.findAll(table).forEach { columns ->
var index = 0
Regexes.MOBIDZIENNIK_ATTENDANCE_COLUMN.findAll(columns[1]).forEach { column ->
if (column[1].contains("colspan")) {
val colspan =
Regexes.MOBIDZIENNIK_ATTENDANCE_COLUMN_SPAN.find(column[1])
?.get(1)
?.toIntOrNull() ?: 0
entries.addAll(index, List(colspan) { "" })
ranges.addAll(List(colspan) { null })
index += colspan
}
else {
val range = Regexes.MOBIDZIENNIK_ATTENDANCE_RANGE.find(column[2])
ranges.add(range)
index++
}
}
}
val dateIterator = lessonDates.iterator()
val entriesIterator = entries.iterator()
Regexes.MOBIDZIENNIK_ATTENDANCE_RANGE.findAll(table).let { ranges ->
val count = ranges.count()
// verify the lesson count is the same as dates & entries
if (count != lessonDates.count() || count != entries.count())
val count = ranges.count()
// verify the lesson count is the same as dates & entries
if (count != lessonDates.count() || count != entries.count())
return@forEach
ranges.forEach { range ->
val lessonDate = dateIterator.next()
val entry = entriesIterator.next()
if (range == null || entry.isBlank())
return@forEach
ranges.forEach { range ->
val lessonDate = dateIterator.next()
var entry = entriesIterator.next()
if (entry.isBlank())
return@forEach
val startTime = Time.fromH_m(range[1])
val startTime = Time.fromH_m(range[1])
range[2].split(" / ").mapNotNull { Regexes.MOBIDZIENNIK_ATTENDANCE_LESSON.find(it) }.forEachIndexed { index, lesson ->
val topic = lesson[1].substringAfter(" - ", missingDelimiterValue = "").takeIf { it.isNotBlank() }
if (topic?.startsWith("Lekcja odwołana: ") == true || entry.isEmpty())
return@forEachIndexed
val subjectName = lesson[1].substringBefore(" - ")
//val team = lesson[3]
val teacherName = lesson[3].fixName()
val teacherId = data.teacherList.singleOrNull { it.fullNameLastFirst == teacherName }?.id ?: -1
val subjectId = data.subjectList.singleOrNull { it.longName == subjectName }?.id ?: -1
var typeSymbol = ""
for (symbol in typeSymbols) {
if (entry.startsWith(symbol) && symbol.length > typeSymbol.length)
typeSymbol = symbol
}
entry = entry.removePrefix(typeSymbol)
var isCounted = true
val baseType = when (typeSymbol) {
"." -> TYPE_PRESENT
"|" -> TYPE_ABSENT
"+" -> TYPE_ABSENT_EXCUSED
"s" -> TYPE_BELATED
"z" -> TYPE_RELEASED
else -> {
isCounted = false
when (typeSymbol) {
"e" -> TYPE_PRESENT_CUSTOM
"en" -> TYPE_ABSENT
"ep" -> TYPE_PRESENT_CUSTOM
else -> TYPE_UNKNOWN
}
}
}
val typeName = types?.get(typeSymbol) ?: ""
val typeColor = when (typeSymbol) {
"e" -> 0xff673ab7
"en" -> 0xffec407a
"ep" -> 0xff4caf50
else -> null
}?.toInt()
val typeShort = if (isCounted)
data.app.attendanceManager.getTypeShort(baseType)
else
typeSymbol
val semester = data.profile?.dateToSemester(lessonDate) ?: 1
val id = lessonDate.combineWith(startTime) / 6L * 10L + (lesson[0].hashCode() and 0xFFFF) + index
val attendanceObject = Attendance(
profileId = profileId,
id = id,
baseType = baseType,
typeName = typeName,
typeShort = typeShort,
typeSymbol = typeSymbol,
typeColor = typeColor,
date = lessonDate,
startTime = startTime,
semester = semester,
teacherId = teacherId,
subjectId = subjectId
).also {
it.lessonTopic = topic
it.isCounted = isCounted
}
data.attendanceList.add(attendanceObject)
if (baseType != TYPE_PRESENT) {
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_ATTENDANCE,
id,
data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == TYPE_UNKNOWN,
data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == TYPE_UNKNOWN
))
}
}
range[2].split(" / ").mapNotNull {
Regexes.MOBIDZIENNIK_ATTENDANCE_LESSON.find(it)
}.forEachIndexed { index, lesson ->
processEntry(
index,
lesson,
lessonDate,
startTime,
entry,
types,
typeSymbols
)
}
}
}
@ -206,4 +159,97 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik,
onSuccess()
}
}
private fun processEntry(
index: Int,
lesson: MatchResult,
lessonDate: Date,
startTime: Time,
entry: String,
types: Map<String?, String?>?,
typeSymbols: List<String>
) {
var entry = entry
val topic = lesson[1].substringAfter(" - ", missingDelimiterValue = "").takeIf { it.isNotBlank() }
if (topic?.startsWith("Lekcja odwołana: ") == true || entry.isEmpty())
return
val subjectName = lesson[1].substringBefore(" - ")
//val team = lesson[3]
val teacherName = lesson[3].fixName()
val teacherId = data.teacherList.singleOrNull { it.fullNameLastFirst == teacherName }?.id ?: -1
val subjectId = data.subjectList.singleOrNull { it.longName == subjectName }?.id ?: -1
var typeSymbol = ""
for (symbol in typeSymbols) {
if (entry.startsWith(symbol) && symbol.length > typeSymbol.length)
typeSymbol = symbol
}
entry = entry.removePrefix(typeSymbol)
var isCounted = true
val baseType = when (typeSymbol) {
"." -> TYPE_PRESENT
"|" -> TYPE_ABSENT
"+" -> TYPE_ABSENT_EXCUSED
"s" -> TYPE_BELATED
"z" -> TYPE_RELEASED
else -> {
isCounted = false
when (typeSymbol) {
"e" -> TYPE_PRESENT_CUSTOM
"en" -> TYPE_ABSENT
"ep" -> TYPE_PRESENT_CUSTOM
else -> TYPE_UNKNOWN
}
}
}
val typeName = types?.get(typeSymbol) ?: ""
val typeColor = when (typeSymbol) {
"e" -> 0xff673ab7
"en" -> 0xffec407a
"ep" -> 0xff4caf50
else -> null
}?.toInt()
val typeShort = if (isCounted)
data.app.attendanceManager.getTypeShort(baseType)
else
typeSymbol
val semester = data.profile?.dateToSemester(lessonDate) ?: 1
val id = lessonDate.combineWith(startTime) / 6L * 10L + (lesson[0].hashCode() and 0xFFFF) + index
val attendanceObject = Attendance(
profileId = profileId,
id = id,
baseType = baseType,
typeName = typeName,
typeShort = typeShort,
typeSymbol = typeSymbol,
typeColor = typeColor,
date = lessonDate,
startTime = startTime,
semester = semester,
teacherId = teacherId,
subjectId = subjectId
).also {
it.lessonTopic = topic
it.isCounted = isCounted
}
data.attendanceList.add(attendanceObject)
if (baseType != TYPE_PRESENT) {
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_ATTENDANCE,
id,
data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == TYPE_UNKNOWN,
data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == TYPE_UNKNOWN
))
}
}
}

View File

@ -56,20 +56,21 @@ class MobidziennikWebGetRecipientList(override val data: DataMobidziennik,
}
private fun processRecipient(listType: Int, listName: String, recipient: JsonObject) {
val id = recipient.getLong("id") ?: -1
val id = recipient.getString("id") ?: return
val idLong = id.replace(Regexes.NOT_DIGITS, "").toLongOrNull() ?: return
// get teacher by ID or create it
val teacher = data.teacherList[id] ?: Teacher(data.profileId, id).apply {
val teacher = data.teacherList[idLong] ?: Teacher(data.profileId, idLong).apply {
val fullName = recipient.getString("nazwa")?.fixName()
name = fullName ?: ""
fullName?.splitName()?.let {
name = it.second
surname = it.first
}
data.teacherList[id] = this
data.teacherList[idLong] = this
}
teacher.apply {
loginId = id.toString()
loginId = id
when (listType) {
1 -> setTeacherType(Teacher.TYPE_PRINCIPAL)
2 -> setTeacherType(Teacher.TYPE_TEACHER)

View File

@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.firstlogin
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_PODLASIE
import pl.szczodrzynski.edziennik.data.api.PODLASIE_API_LOGOUT_DEVICES_ENDPOINT
import pl.szczodrzynski.edziennik.data.api.PODLASIE_API_USER_ENDPOINT
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.PodlasieApi
@ -22,50 +23,62 @@ class PodlasieFirstLogin(val data: DataPodlasie, val onSuccess: () -> Unit) {
private val api = PodlasieApi(data, null)
init {
PodlasieLoginApi(data) {
doLogin()
}
}
private fun doLogin() {
val loginStoreId = data.loginStore.id
val loginStoreType = LOGIN_TYPE_PODLASIE
PodlasieLoginApi(data) {
api.apiGet(TAG, PODLASIE_API_USER_ENDPOINT) { json ->
val uuid = json.getString("Uuid")
val login = json.getString("Login")
val firstName = json.getString("FirstName")
val lastName = json.getString("LastName")
val studentNameLong = "$firstName $lastName".fixName()
val studentNameShort = studentNameLong.getShortName()
val schoolName = json.getString("SchoolName")
val className = json.getString("SchoolClass")
val schoolYear = json.getString("ActualSchoolYear")?.replace(' ', '/')
val semester = json.getString("ActualTermShortcut")?.length
val apiUrl = json.getString("URL")
val profile = Profile(
loginStoreId,
loginStoreId,
loginStoreType,
studentNameLong,
login,
studentNameLong,
studentNameShort,
null
).apply {
studentData["studentId"] = uuid
studentData["studentLogin"] = login
studentData["schoolName"] = schoolName
studentData["className"] = className
studentData["schoolYear"] = schoolYear
studentData["currentSemester"] = semester ?: 1
studentData["apiUrl"] = apiUrl
schoolYear?.split('/')?.get(0)?.toInt()?.let {
studentSchoolYearStart = it
}
studentClassName = className
}
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(listOf(profile), data.loginStore))
onSuccess()
if (data.loginStore.getLoginData("logoutDevices", false)) {
data.loginStore.removeLoginData("logoutDevices")
api.apiGet(TAG, PODLASIE_API_LOGOUT_DEVICES_ENDPOINT) {
doLogin()
}
return
}
api.apiGet(TAG, PODLASIE_API_USER_ENDPOINT) { json ->
val uuid = json.getString("Uuid")
val login = json.getString("Login")
val firstName = json.getString("FirstName")
val lastName = json.getString("LastName")
val studentNameLong = "$firstName $lastName".fixName()
val studentNameShort = studentNameLong.getShortName()
val schoolName = json.getString("SchoolName")
val className = json.getString("SchoolClass")
val schoolYear = json.getString("ActualSchoolYear")?.replace(' ', '/')
val semester = json.getString("ActualTermShortcut")?.length
val apiUrl = json.getString("URL")
val profile = Profile(
loginStoreId,
loginStoreId,
loginStoreType,
studentNameLong,
login,
studentNameLong,
studentNameShort,
null
).apply {
studentData["studentId"] = uuid
studentData["studentLogin"] = login
studentData["schoolName"] = schoolName
studentData["className"] = className
studentData["schoolYear"] = schoolYear
studentData["currentSemester"] = semester ?: 1
studentData["apiUrl"] = apiUrl
schoolYear?.split('/')?.get(0)?.toInt()?.let {
studentSchoolYearStart = it
}
studentClassName = className
}
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(listOf(profile), data.loginStore))
onSuccess()
}
}
}

View File

@ -4,16 +4,16 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.currentTimeUnix
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_WEB_MAIN
import pl.szczodrzynski.edziennik.data.api.LOGIN_MODE_VULCAN_API
import pl.szczodrzynski.edziennik.data.api.models.Data
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.values
class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) {
@ -26,17 +26,27 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
&& apiFingerprint[symbol].isNotNullNorEmpty()
&& apiPrivateKey[symbol].isNotNullNorEmpty()
&& symbol.isNotNullNorEmpty()
fun isHebeLoginValid() = hebePublicKey.isNotNullNorEmpty()
&& hebePrivateKey.isNotNullNorEmpty()
&& symbol.isNotNullNorEmpty()
override fun satisfyLoginMethods() {
loginMethods.clear()
if (isWebMainLoginValid()) {
loginMethods += LOGIN_METHOD_VULCAN_WEB_MAIN
}
if (isApiLoginValid()) {
loginMethods += LOGIN_METHOD_VULCAN_API
}
if (isHebeLoginValid()) {
loginMethods += LOGIN_METHOD_VULCAN_HEBE
}
}
init {
// during the first sync `profile.studentClassName` is already set
if (teamList.values().none { it.type == Team.TYPE_CLASS }) {
if (loginStore.mode == LOGIN_MODE_VULCAN_API
&& teamList.values().none { it.type == Team.TYPE_CLASS }) {
profile?.studentClassName?.also { name ->
val id = Utils.crc16(name.toByteArray()).toLong()
@ -55,6 +65,17 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
override fun generateUserCode() = "$schoolCode:$studentId"
fun buildDeviceId(): String {
val deviceId = app.deviceId.padStart(16, '0')
val loginStoreId = loginStore.id.toString(16).padStart(4, '0')
val symbol = symbol?.crc16()?.toString(16)?.take(2) ?: "00"
return deviceId.substring(0..7) +
"-" + deviceId.substring(8..11) +
"-" + deviceId.substring(12..15) +
"-" + loginStoreId +
"-" + symbol + "6f72616e7a"
}
/**
* A UONET+ client symbol.
*
@ -139,6 +160,16 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
get() { mStudentSemesterId = mStudentSemesterId ?: profile?.getStudentData("studentSemesterId", 0); return mStudentSemesterId ?: 0 }
set(value) { profile?.putStudentData("studentSemesterId", value) ?: return; mStudentSemesterId = value }
private var mStudentUnitId: Int? = null
var studentUnitId: Int
get() { mStudentUnitId = mStudentUnitId ?: profile?.getStudentData("studentUnitId", 0); return mStudentUnitId ?: 0 }
set(value) { profile?.putStudentData("studentUnitId", value) ?: return; mStudentUnitId = value }
private var mStudentConstituentId: Int? = null
var studentConstituentId: Int
get() { mStudentConstituentId = mStudentConstituentId ?: profile?.getStudentData("studentConstituentId", 0); return mStudentConstituentId ?: 0 }
set(value) { profile?.putStudentData("studentConstituentId", value) ?: return; mStudentConstituentId = value }
private var mSemester1Id: Int? = null
var semester1Id: Int
get() { mSemester1Id = mSemester1Id ?: profile?.getStudentData("semester1Id", 0); return mSemester1Id ?: 0 }
@ -203,6 +234,32 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
get() { mApiPrivateKey = mApiPrivateKey ?: loginStore.getLoginData("apiPrivateKey", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mApiPrivateKey ?: mapOf() }
set(value) { loginStore.putLoginData("apiPrivateKey", app.gson.toJson(value)); mApiPrivateKey = value }
/* _ _ _ _____ _____
| | | | | | /\ | __ \_ _|
| |__| | ___| |__ ___ / \ | |__) || |
| __ |/ _ \ '_ \ / _ \ / /\ \ | ___/ | |
| | | | __/ |_) | __/ / ____ \| | _| |_
|_| |_|\___|_.__/ \___| /_/ \_\_| |____*/
private var mHebePublicKey: String? = null
var hebePublicKey: String?
get() { mHebePublicKey = mHebePublicKey ?: loginStore.getLoginData("hebePublicKey", null); return mHebePublicKey }
set(value) { loginStore.putLoginData("hebePublicKey", value); mHebePublicKey = value }
private var mHebePrivateKey: String? = null
var hebePrivateKey: String?
get() { mHebePrivateKey = mHebePrivateKey ?: loginStore.getLoginData("hebePrivateKey", null); return mHebePrivateKey }
set(value) { loginStore.putLoginData("hebePrivateKey", value); mHebePrivateKey = value }
private var mHebePublicHash: String? = null
var hebePublicHash: String?
get() { mHebePublicHash = mHebePublicHash ?: loginStore.getLoginData("hebePublicHash", null); return mHebePublicHash }
set(value) { loginStore.putLoginData("hebePublicHash", value); mHebePublicHash = value }
private var mHebeContext: String? = null
var hebeContext: String?
get() { mHebeContext = mHebeContext ?: profile?.getStudentData("hebeContext", null); return mHebeContext }
set(value) { profile?.putStudentData("hebeContext", value) ?: return; mHebeContext = value }
val apiUrl: String?
get() {
val url = when (apiToken[symbol]?.substring(0, 3)) {
@ -219,6 +276,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
"P01" -> "http://efeb-komunikacja.pro-hudson.win.vulcan.pl"
"P02" -> "http://efeb-komunikacja.pro-hudsonrc.win.vulcan.pl"
"P90" -> "http://efeb-komunikacja-pro-mwujakowska.neo.win.vulcan.pl"
"KO1" -> "https://uonetplus-komunikacja.eduportal.koszalin.pl"
"FK1", "FS1" -> "http://api.fakelog.cf"
"SZ9" -> "http://hack.szkolny.eu"
else -> null
@ -226,7 +284,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
return if (url != null) "$url/$symbol/" else loginStore.getLoginData("apiUrl", null)
}
val fullApiUrl: String?
val fullApiUrl: String
get() {
return "$apiUrl$schoolSymbol/"
}

View File

@ -13,6 +13,8 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanData
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiAttachments
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiMessagesChangeStatus
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiSendMessage
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.VulcanHebeMessagesChangeStatus
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.VulcanHebeSendMessage
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.firstlogin.VulcanFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLogin
import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent
@ -91,6 +93,20 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
override fun getMessage(message: MessageFull) {
if (loginStore.mode != LOGIN_MODE_VULCAN_API) {
login(LOGIN_METHOD_VULCAN_HEBE) {
if (message.seen) {
EventBus.getDefault().postSticky(MessageGetEvent(message))
completed()
return@login
}
VulcanHebeMessagesChangeStatus(data, message) {
completed()
}
}
return
}
login(LOGIN_METHOD_VULCAN_API) {
if (message.attachmentIds != null) {
VulcanApiMessagesChangeStatus(data, message) {
@ -120,6 +136,15 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
override fun sendMessage(recipients: List<Teacher>, subject: String, text: String) {
if (loginStore.mode != LOGIN_MODE_VULCAN_API) {
login(LOGIN_METHOD_VULCAN_HEBE) {
VulcanHebeSendMessage(data, recipients, subject, text) {
completed()
}
}
return
}
login(LOGIN_METHOD_VULCAN_API) {
VulcanApiSendMessage(data, recipients, subject, text) {
completed()

View File

@ -19,34 +19,68 @@ const val ENDPOINT_VULCAN_API_NOTICES = 1070
const val ENDPOINT_VULCAN_API_ATTENDANCE = 1080
const val ENDPOINT_VULCAN_API_MESSAGES_INBOX = 1090
const val ENDPOINT_VULCAN_API_MESSAGES_SENT = 1100
const val ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS = 2010
const val ENDPOINT_VULCAN_HEBE_MAIN = 3000
const val ENDPOINT_VULCAN_HEBE_PUSH_CONFIG = 3005
const val ENDPOINT_VULCAN_HEBE_ADDRESSBOOK = 3010
const val ENDPOINT_VULCAN_HEBE_TIMETABLE = 3020
const val ENDPOINT_VULCAN_HEBE_EXAMS = 3030
const val ENDPOINT_VULCAN_HEBE_GRADES = 3040
const val ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY = 3050
const val ENDPOINT_VULCAN_HEBE_HOMEWORK = 3060
const val ENDPOINT_VULCAN_HEBE_NOTICES = 3070
const val ENDPOINT_VULCAN_HEBE_ATTENDANCE = 3080
const val ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX = 3090
const val ENDPOINT_VULCAN_HEBE_MESSAGES_SENT = 3100
const val ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER = 3200
val VulcanFeatures = listOf(
// timetable
Feature(LOGIN_TYPE_VULCAN, FEATURE_TIMETABLE, listOf(
ENDPOINT_VULCAN_API_TIMETABLE to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_TIMETABLE, listOf(
ENDPOINT_VULCAN_HEBE_TIMETABLE to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
// agenda
Feature(LOGIN_TYPE_VULCAN, FEATURE_AGENDA, listOf(
ENDPOINT_VULCAN_API_EVENTS to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_AGENDA, listOf(
ENDPOINT_VULCAN_HEBE_EXAMS to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
// grades
Feature(LOGIN_TYPE_VULCAN, FEATURE_GRADES, listOf(
ENDPOINT_VULCAN_API_GRADES to LOGIN_METHOD_VULCAN_API,
ENDPOINT_VULCAN_API_GRADES_SUMMARY to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_GRADES, listOf(
ENDPOINT_VULCAN_HEBE_GRADES to LOGIN_METHOD_VULCAN_HEBE,
ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
// homework
Feature(LOGIN_TYPE_VULCAN, FEATURE_HOMEWORK, listOf(
ENDPOINT_VULCAN_API_HOMEWORK to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_HOMEWORK, listOf(
ENDPOINT_VULCAN_HEBE_HOMEWORK to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
// behaviour
Feature(LOGIN_TYPE_VULCAN, FEATURE_BEHAVIOUR, listOf(
ENDPOINT_VULCAN_API_NOTICES to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_BEHAVIOUR, listOf(
ENDPOINT_VULCAN_HEBE_NOTICES to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
// attendance
Feature(LOGIN_TYPE_VULCAN, FEATURE_ATTENDANCE, listOf(
ENDPOINT_VULCAN_API_ATTENDANCE to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_ATTENDANCE, listOf(
ENDPOINT_VULCAN_HEBE_ATTENDANCE to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
// messages
Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_INBOX, listOf(
ENDPOINT_VULCAN_API_MESSAGES_INBOX to LOGIN_METHOD_VULCAN_API
@ -54,6 +88,12 @@ val VulcanFeatures = listOf(
Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_SENT, listOf(
ENDPOINT_VULCAN_API_MESSAGES_SENT to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_INBOX, listOf(
ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_SENT, listOf(
ENDPOINT_VULCAN_HEBE_MESSAGES_SENT to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
// push config
Feature(LOGIN_TYPE_VULCAN, FEATURE_PUSH_CONFIG, listOf(
@ -61,18 +101,37 @@ val VulcanFeatures = listOf(
), listOf(LOGIN_METHOD_VULCAN_API)).withShouldSync { data ->
!data.app.config.sync.tokenVulcanList.contains(data.profileId)
},
Feature(LOGIN_TYPE_VULCAN, FEATURE_PUSH_CONFIG, listOf(
ENDPOINT_VULCAN_HEBE_PUSH_CONFIG to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)).withShouldSync { data ->
!data.app.config.sync.tokenVulcanList.contains(data.profileId)
},
/**
* Lucky number - using WEB Main.
*/
Feature(LOGIN_TYPE_VULCAN, FEATURE_LUCKY_NUMBER, listOf(
ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS to LOGIN_METHOD_VULCAN_WEB_MAIN
), listOf(LOGIN_METHOD_VULCAN_WEB_MAIN)).withShouldSync { data -> data.shouldSyncLuckyNumber() },
), listOf(LOGIN_METHOD_VULCAN_WEB_MAIN))
.withShouldSync { data -> data.shouldSyncLuckyNumber() }
.withPriority(2),
/**
* Lucky number - using Hebe API
*/
Feature(LOGIN_TYPE_VULCAN, FEATURE_LUCKY_NUMBER, listOf(
ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE))
.withShouldSync { data -> data.shouldSyncLuckyNumber() }
.withPriority(1),
Feature(LOGIN_TYPE_VULCAN, FEATURE_ALWAYS_NEEDED, listOf(
ENDPOINT_VULCAN_API_UPDATE_SEMESTER to LOGIN_METHOD_VULCAN_API,
ENDPOINT_VULCAN_API_DICTIONARIES to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API))
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_ALWAYS_NEEDED, listOf(
ENDPOINT_VULCAN_HEBE_MAIN to LOGIN_METHOD_VULCAN_HEBE,
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE))
/*Feature(LOGIN_TYPE_VULCAN, FEATURE_STUDENT_INFO, listOf(
ENDPOINT_VULCAN_API to LOGIN_METHOD_VULCAN_WEB
), listOf(LOGIN_METHOD_VULCAN_WEB)),

View File

@ -5,9 +5,13 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_API_DEPRECATED
import pl.szczodrzynski.edziennik.data.api.LOGIN_MODE_VULCAN_API
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.web.VulcanWebLuckyNumber
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.utils.Utils
class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
@ -15,9 +19,38 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
private const val TAG = "VulcanData"
}
init {
nextEndpoint(onSuccess)
}
private var firstSemesterSync = false
private val firstSemesterSyncExclude = listOf(
ENDPOINT_VULCAN_HEBE_MAIN,
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK,
ENDPOINT_VULCAN_HEBE_TIMETABLE,
ENDPOINT_VULCAN_HEBE_EXAMS,
ENDPOINT_VULCAN_HEBE_HOMEWORK,
ENDPOINT_VULCAN_HEBE_NOTICES,
ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX,
ENDPOINT_VULCAN_HEBE_MESSAGES_SENT
)
init { run {
if (data.loginStore.mode == LOGIN_MODE_VULCAN_API) {
data.error(TAG, ERROR_VULCAN_API_DEPRECATED)
return@run
}
if (data.studentSemesterNumber == 2 && data.profile?.empty != false) {
firstSemesterSync = true
// set to sync 1st semester first
data.studentSemesterId = data.semester1Id
data.studentSemesterNumber = 1
}
nextEndpoint {
if (firstSemesterSync) {
// at the end, set back 2nd semester
data.studentSemesterId = data.semester2Id
data.studentSemesterNumber = 2
}
onSuccess()
}
}}
private fun nextEndpoint(onSuccess: () -> Unit) {
if (data.targetEndpointIds.isEmpty()) {
@ -30,7 +63,21 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
}
val id = data.targetEndpointIds.firstKey()
val lastSync = data.targetEndpointIds.remove(id)
useEndpoint(id, lastSync) { endpointId ->
useEndpoint(id, lastSync) {
if (firstSemesterSync && id !in firstSemesterSyncExclude) {
// sync 2nd semester after every endpoint
data.studentSemesterId = data.semester2Id
data.studentSemesterNumber = 2
useEndpoint(id, lastSync) {
// set 1st semester back for the next endpoint
data.studentSemesterId = data.semester1Id
data.studentSemesterNumber = 1
// progress further
data.progress(data.progressStep)
nextEndpoint(onSuccess)
}
return@useEndpoint
}
data.progress(data.progressStep)
nextEndpoint(onSuccess)
}
@ -91,6 +138,67 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_lucky_number)
VulcanWebLuckyNumber(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_MAIN -> {
if (data.profile == null) {
onSuccess(ENDPOINT_VULCAN_HEBE_MAIN)
return
}
data.startProgress(R.string.edziennik_progress_endpoint_student_info)
VulcanHebeMain(data, lastSync).getStudents(
profile = data.profile,
profileList = null
) {
onSuccess(ENDPOINT_VULCAN_HEBE_MAIN)
}
}
ENDPOINT_VULCAN_HEBE_PUSH_CONFIG -> {
data.startProgress(R.string.edziennik_progress_endpoint_push_config)
VulcanHebePushConfig(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK -> {
data.startProgress(R.string.edziennik_progress_endpoint_teachers)
VulcanHebeAddressbook(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_TIMETABLE -> {
data.startProgress(R.string.edziennik_progress_endpoint_timetable)
VulcanHebeTimetable(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_EXAMS -> {
data.startProgress(R.string.edziennik_progress_endpoint_exams)
VulcanHebeExams(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_GRADES -> {
data.startProgress(R.string.edziennik_progress_endpoint_grades)
VulcanHebeGrades(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY -> {
data.startProgress(R.string.edziennik_progress_endpoint_proposed_grades)
VulcanHebeGradeSummary(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_HOMEWORK -> {
data.startProgress(R.string.edziennik_progress_endpoint_homework)
VulcanHebeHomework(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_NOTICES -> {
data.startProgress(R.string.edziennik_progress_endpoint_notices)
VulcanHebeNotices(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_ATTENDANCE -> {
data.startProgress(R.string.edziennik_progress_endpoint_attendance)
VulcanHebeAttendance(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox)
VulcanHebeMessages(data, lastSync, onSuccess).getMessages(Message.TYPE_RECEIVED)
}
ENDPOINT_VULCAN_HEBE_MESSAGES_SENT -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_outbox)
VulcanHebeMessages(data, lastSync, onSuccess).getMessages(Message.TYPE_SENT)
}
ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER -> {
data.startProgress(R.string.edziennik_progress_endpoint_lucky_number)
VulcanHebeLuckyNumber(data, lastSync, onSuccess)
}
else -> onSuccess(endpointId)
}
}

View File

@ -0,0 +1,393 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-20.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data
import android.os.Build
import androidx.core.util.set
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.body.MediaTypeUtils
import im.wangchao.mhttp.callback.JsonCallbackHandler
import io.github.wulkanowy.signer.hebe.getSignatureHeaders
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.HebeFilterType
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.entity.LessonRange
import pl.szczodrzynski.edziennik.data.db.entity.Subject
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import java.net.HttpURLConnection
import java.net.URLEncoder
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.*
open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
companion object {
const val TAG = "VulcanHebe"
}
val profileId
get() = data.profile?.id ?: -1
val profile
get() = data.profile
fun getDateTime(
json: JsonObject?,
key: String,
default: Long = System.currentTimeMillis()
): Long {
val date = json.getJsonObject(key)
return date.getLong("Timestamp") ?: return default
}
fun getDate(json: JsonObject?, key: String): Date? {
val date = json.getJsonObject(key)
return date.getString("Date")?.let { Date.fromY_m_d(it) }
}
fun getTeacherId(json: JsonObject?, key: String): Long? {
val teacher = json.getJsonObject(key)
val teacherId = teacher.getLong("Id") ?: return null
if (data.teacherList[teacherId] == null) {
data.teacherList[teacherId] = Teacher(
data.profileId,
teacherId,
teacher.getString("Name") ?: "",
teacher.getString("Surname") ?: ""
)
}
return teacherId
}
fun getSubjectId(json: JsonObject?, key: String): Long? {
val subject = json.getJsonObject(key)
val subjectId = subject.getLong("Id") ?: return null
if (data.subjectList[subjectId] == null) {
data.subjectList[subjectId] = Subject(
data.profileId,
subjectId,
subject.getString("Name") ?: "",
subject.getString("Kod") ?: ""
)
}
return subjectId
}
fun getTeamId(json: JsonObject?, key: String): Long? {
val team = json.getJsonObject(key)
val teamId = team.getLong("Id") ?: return null
if (data.teamList[teamId] == null) {
var name = team.getString("Shortcut")
?: team.getString("Name")
?: ""
name = "${profile?.studentClassName ?: ""} $name"
data.teamList[teamId] = Team(
data.profileId,
teamId,
name,
Team.TYPE_VIRTUAL,
"${data.schoolCode}:$name",
-1
)
}
return teamId
}
fun getClassId(json: JsonObject?, key: String): Long? {
val team = json.getJsonObject(key)
val teamId = team.getLong("Id") ?: return null
if (data.teamList[teamId] == null) {
val name = data.profile?.studentClassName
?: team.getString("Name")
?: ""
data.teamList[teamId] = Team(
data.profileId,
teamId,
name,
Team.TYPE_CLASS,
"${data.schoolCode}:$name",
-1
)
}
return teamId
}
fun getLessonRange(json: JsonObject?, key: String): LessonRange? {
val timeslot = json.getJsonObject(key)
val position = timeslot.getInt("Position") ?: return null
val start = timeslot.getString("Start") ?: return null
val end = timeslot.getString("End") ?: return null
val lessonRange = LessonRange(
data.profileId,
position,
Time.fromH_m(start),
Time.fromH_m(end)
)
data.lessonRanges[position] = lessonRange
return lessonRange
}
fun getSemester(json: JsonObject?): Int {
val periodId = json.getInt("PeriodId") ?: return 1
return if (periodId == data.semester1Id)
1
else
2
}
fun isCurrentYear(date: Date): Boolean {
return profile?.let { profile ->
return@let date >= profile.dateSemester1Start
} ?: false
}
fun isCurrentYear(dateTime: Long): Boolean {
return profile?.let { profile ->
return@let dateTime >= profile.dateSemester1Start.inMillis
} ?: false
}
inline fun <reified T> apiRequest(
tag: String,
endpoint: String,
method: Int = GET,
payload: JsonElement? = null,
baseUrl: Boolean = false,
firebaseToken: String? = null,
crossinline onSuccess: (json: T, response: Response?) -> Unit
) {
val url = "${if (baseUrl) data.apiUrl else data.fullApiUrl}$endpoint"
d(tag, "Request: Vulcan/Hebe - $url")
val privateKey = data.hebePrivateKey
val publicHash = data.hebePublicHash
if (privateKey == null || publicHash == null) {
data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING))
return
}
val timestamp = ZonedDateTime.now(ZoneId.of("GMT"))
val timestampMillis = timestamp.toInstant().toEpochMilli()
val timestampIso = timestamp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"))
val finalPayload = if (payload != null) {
JsonObject(
"AppName" to VULCAN_HEBE_APP_NAME,
"AppVersion" to VULCAN_HEBE_APP_VERSION,
"CertificateId" to publicHash,
"Envelope" to payload,
"FirebaseToken" to (firebaseToken ?: data.app.config.sync.tokenVulcanHebe),
"API" to 1,
"RequestId" to UUID.randomUUID().toString(),
"Timestamp" to timestampMillis,
"TimestampFormatted" to timestampIso
)
} else null
val jsonString = finalPayload?.toString()
val headers = getSignatureHeaders(
publicHash,
privateKey,
jsonString,
endpoint,
timestamp
)
val callback = object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response?) {
if (json == null) {
data.error(
ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response)
)
return
}
val status = json.getJsonObject("Status")
if (status?.getInt("Code") != 0) {
data.error(
ApiError(tag, ERROR_VULCAN_HEBE_OTHER)
.withResponse(response)
.withApiResponse(json.toString())
)
}
val envelope = if (json.get("Envelope").isJsonNull && null is T)
null as T
else when (T::class.java) {
JsonObject::class.java -> json.getJsonObject("Envelope") as T
JsonArray::class.java -> json.getJsonArray("Envelope") as T
java.lang.Boolean::class.java -> json.getBoolean("Envelope") as T
else -> {
data.error(
ApiError(tag, ERROR_RESPONSE_EMPTY)
.withResponse(response)
.withApiResponse(json)
)
return
}
}
try {
onSuccess(envelope, response)
} catch (e: Exception) {
data.error(
ApiError(tag, EXCEPTION_VULCAN_HEBE_REQUEST)
.withResponse(response)
.withThrowable(e)
.withApiResponse(json)
)
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(
ApiError(tag, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable)
)
}
}
Request.builder()
.url(url)
.userAgent(VULCAN_HEBE_USER_AGENT)
.addHeader("vOS", "Android")
.addHeader("vDeviceModel", Build.MODEL)
.addHeader("vAPI", "1")
.apply {
if (data.hebeContext != null)
addHeader("vContext", data.hebeContext)
headers.forEach {
addHeader(it.key, it.value)
}
when (method) {
GET -> get()
POST -> {
post()
setTextBody(jsonString, MediaTypeUtils.APPLICATION_JSON)
}
}
}
.allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST)
.allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN)
.allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED)
.allowErrorCode(HttpURLConnection.HTTP_UNAVAILABLE)
.callback(callback)
.build()
.enqueue()
}
inline fun <reified T> apiGet(
tag: String,
endpoint: String,
query: Map<String, String> = mapOf(),
baseUrl: Boolean = false,
firebaseToken: String? = null,
crossinline onSuccess: (json: T, response: Response?) -> Unit
) {
val queryPath = query.map {
it.key + "=" + URLEncoder.encode(it.value, "UTF-8").replace("+", "%20")
}.join("&")
apiRequest(
tag,
if (query.isNotEmpty()) "$endpoint?$queryPath" else endpoint,
baseUrl = baseUrl,
firebaseToken = firebaseToken,
onSuccess = onSuccess
)
}
inline fun <reified T> apiPost(
tag: String,
endpoint: String,
payload: JsonElement,
baseUrl: Boolean = false,
firebaseToken: String? = null,
crossinline onSuccess: (json: T, response: Response?) -> Unit
) {
apiRequest(
tag,
endpoint,
method = POST,
payload,
baseUrl = baseUrl,
firebaseToken = firebaseToken,
onSuccess = onSuccess
)
}
fun apiGetList(
tag: String,
endpoint: String,
filterType: HebeFilterType? = null,
dateFrom: Date? = null,
dateTo: Date? = null,
lastSync: Long? = null,
folder: Int? = null,
params: Map<String, String> = mapOf(),
includeFilterType: Boolean = true,
onSuccess: (data: List<JsonObject>, response: Response?) -> Unit
) {
val url = if (includeFilterType && filterType != null)
"$endpoint/${filterType.endpoint}"
else endpoint
val query = params.toMutableMap()
when (filterType) {
HebeFilterType.BY_PUPIL -> {
query["unitId"] = data.studentUnitId.toString()
query["pupilId"] = data.studentId.toString()
query["periodId"] = data.studentSemesterId.toString()
}
HebeFilterType.BY_PERSON -> {
query["loginId"] = data.studentLoginId.toString()
}
HebeFilterType.BY_PERIOD -> {
query["periodId"] = data.studentSemesterId.toString()
query["pupilId"] = data.studentId.toString()
}
}
if (dateFrom != null)
query["dateFrom"] = dateFrom.stringY_m_d
if (dateTo != null)
query["dateTo"] = dateTo.stringY_m_d
if (folder != null)
query["folder"] = folder.toString()
val semester1Start = profile?.dateSemester1Start?.inMillis
query["lastId"] = "-2147483648" // don't ask, it's just Vulcan
query["pageSize"] = "500"
query["lastSyncDate"] = LocalDateTime
.ofInstant(
Instant.ofEpochMilli(lastSync ?: semester1Start ?: 0),
ZoneId.systemDefault()
)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
apiGet(tag, url, query) { json: JsonArray, response ->
onSuccess(json.map { it.asJsonObject }, response)
}
}
}

View File

@ -144,7 +144,7 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
var count = 0
while (clientIndex != -1 && count < 100) {
val startIndex = clientIndex + clientUrl.length
val endIndex = text.indexOf('/', startIndex = startIndex)
val endIndex = text.indexOf('"', startIndex = startIndex)
val schoolSymbol = text.substring(startIndex, endIndex)
schoolSymbols += schoolSymbol
clientIndex = text.indexOf(clientUrl, startIndex = endIndex)

View File

@ -0,0 +1,11 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-20.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
enum class HebeFilterType(val endpoint: String) {
BY_PUPIL("byPupil"),
BY_PERSON("byPerson"),
BY_PERIOD("byPeriod")
}

View File

@ -0,0 +1,122 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import androidx.core.util.set
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_ADDRESSBOOK
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_ADDRESSBOOK
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_EDUCATOR
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_OTHER
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_PARENT
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_PARENTS_COUNCIL
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_STUDENT
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_TEACHER
import kotlin.text.replace
class VulcanHebeAddressbook(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeAddressbook"
}
private fun String.removeUnitName(unitName: String?): String {
return (unitName ?: data.schoolShort)?.let {
this.replace("($it)", "").trim()
} ?: this
}
init {
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_ADDRESSBOOK,
HebeFilterType.BY_PERSON,
lastSync = lastSync,
includeFilterType = false
) { list, _ ->
list.forEach { person ->
val id = person.getString("Id") ?: return@forEach
val loginId = person.getString("LoginId") ?: return@forEach
val idType = id.split("-")
.getOrNull(0)
val idLong = id.split("-")
.getOrNull(1)
?.toLongOrNull()
?: return@forEach
val typeBase = when (idType) {
"e" -> TYPE_TEACHER
"c" -> TYPE_PARENT
"p" -> TYPE_STUDENT
else -> TYPE_OTHER
}
val name = person.getString("Name") ?: ""
val surname = person.getString("Surname") ?: ""
val namePrefix = "$surname $name - "
val teacher = data.teacherList[idLong] ?: Teacher(
data.profileId,
idLong,
name,
surname,
loginId
).also {
data.teacherList[idLong] = it
}
person.getJsonArray("Roles")?.asJsonObjectList()?.onEach { role ->
var roleText: String? = null
val unitName = role.getString("ConstituentUnitSymbol")
val personType = when (role.getInt("RoleOrder")) {
0 -> { /* Wychowawca */
roleText = role.getString("ClassSymbol")
?.removeUnitName(unitName)
TYPE_EDUCATOR
}
1 -> TYPE_TEACHER /* Nauczyciel */
2 -> return@onEach /* Pracownik */
3 -> { /* Rada rodziców */
roleText = role.getString("Address")
?.removeUnitName(unitName)
?.removePrefix(namePrefix)
?.trim()
TYPE_PARENTS_COUNCIL
}
5 -> {
roleText = role.getString("RoleName")
?.plus(" - ")
?.plus(
role.getString("Address")
?.removeUnitName(unitName)
?.removePrefix(namePrefix)
?.trim()
)
TYPE_STUDENT
}
else -> TYPE_OTHER
}
teacher.setTeacherType(personType)
teacher.typeDescription = roleText
}
if (teacher.type == 0)
teacher.setTeacherType(typeBase)
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK, 2 * DAY)
onSuccess(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK)
}
}
}

View File

@ -0,0 +1,144 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2021-2-21
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_ATTENDANCE
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_ATTENDANCE
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
class VulcanHebeAttendance(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeAttendance"
}
init {
val semesterNumber = data.studentSemesterNumber
val startDate = profile?.getSemesterStart(semesterNumber)
val endDate = profile?.getSemesterEnd(semesterNumber)
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_ATTENDANCE,
HebeFilterType.BY_PUPIL,
dateFrom = startDate,
dateTo = endDate,
lastSync = lastSync
) { list, _ ->
list.forEach { attendance ->
val id = attendance.getLong("AuxPresenceId") ?: return@forEach
val type = attendance.getJsonObject("PresenceType") ?: return@forEach
val baseType = getBaseType(type)
val typeName = type.getString("Name") ?: return@forEach
val typeCategoryId = type.getLong("CategoryId") ?: return@forEach
val typeSymbol = type.getString("Symbol") ?: return@forEach
val typeShort = when (typeCategoryId.toInt()) {
6, 8 -> typeSymbol
else -> data.app.attendanceManager.getTypeShort(baseType)
}
val typeColor = when (typeCategoryId.toInt()) {
1 -> 0xffffffff // obecność
2 -> 0xffffa687 // nieobecność
3 -> 0xfffcc150 // nieobecność usprawiedliwiona
4 -> 0xffede049 // spóźnienie
5 -> 0xffbbdd5f // spóźnienie usprawiedliwione
6 -> 0xffa9c9fd // nieobecny z przyczyn szkolnych
7 -> 0xffddbbe5 // zwolniony
8 -> 0xffffffff // usunięty wpis
else -> null
}?.toInt()
val date = getDate(attendance, "Day") ?: return@forEach
val lessonRange = getLessonRange(attendance, "TimeSlot")
val startTime = lessonRange?.startTime
val semester = profile?.dateToSemester(date) ?: return@forEach
val teacherId = attendance.getJsonObject("TeacherPrimary")?.getLong("Id") ?: -1
val subjectId = attendance.getJsonObject("Subject")?.getLong("Id") ?: -1
val addedDate = getDateTime(attendance, "DateModify")
val lessonNumber = lessonRange?.lessonNumber
val isCounted = attendance.getBoolean("CalculatePresence")
?: (baseType != Attendance.TYPE_RELEASED)
val attendanceObject = Attendance(
profileId = profileId,
id = id,
baseType = baseType,
typeName = typeName,
typeShort = typeShort,
typeSymbol = typeSymbol,
typeColor = typeColor,
date = date,
startTime = startTime,
semester = semester,
teacherId = teacherId,
subjectId = subjectId,
addedDate = addedDate
).also {
it.lessonTopic = attendance.getString("Topic")
it.lessonNumber = lessonNumber
it.isCounted = isCounted
}
data.attendanceList.add(attendanceObject)
if (baseType != Attendance.TYPE_PRESENT) {
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_ATTENDANCE,
attendanceObject.id,
profile?.empty ?: true
|| baseType == Attendance.TYPE_PRESENT_CUSTOM
|| baseType == Attendance.TYPE_UNKNOWN,
profile?.empty ?: true
|| baseType == Attendance.TYPE_PRESENT_CUSTOM
|| baseType == Attendance.TYPE_UNKNOWN
)
)
}
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_ATTENDANCE, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_HEBE_ATTENDANCE)
}
}
fun getBaseType(attendanceType: JsonObject): Int {
val absent = attendanceType.getBoolean("Absence") ?: false
val excused = attendanceType.getBoolean("AbsenceJustified") ?: false
return if (absent) {
if (excused)
Attendance.TYPE_ABSENT_EXCUSED
else
Attendance.TYPE_ABSENT
} else {
val belated = attendanceType.getBoolean("Late") ?: false
val released = attendanceType.getBoolean("LegalAbsence") ?: false
val present = attendanceType.getBoolean("Presence") ?: true
if (belated)
if (excused)
Attendance.TYPE_BELATED_EXCUSED
else
Attendance.TYPE_BELATED
else if (released)
Attendance.TYPE_RELEASED
else if (present)
if (attendanceType.getInt("CategoryId") != 1)
Attendance.TYPE_PRESENT_CUSTOM
else
Attendance.TYPE_PRESENT
else
Attendance.TYPE_UNKNOWN
}
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_EXAMS
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_EXAMS
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
class VulcanHebeExams(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeExams"
}
init {
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_EXAMS,
HebeFilterType.BY_PUPIL,
lastSync = lastSync
) { list, _ ->
list.forEach { exam ->
val id = exam.getLong("Id") ?: return@forEach
val eventDate = getDate(exam, "Deadline") ?: return@forEach
val subjectId = getSubjectId(exam, "Subject") ?: -1
val teacherId = getTeacherId(exam, "Creator") ?: -1
val teamId = getTeamId(exam, "Distribution")
?: getClassId(exam, "Class")
?: data.teamClass?.id
?: -1
val topic = exam.getString("Content")?.trim() ?: ""
if (!isCurrentYear(eventDate)) return@forEach
val lessonList = data.db.timetableDao().getAllForDateNow(profileId, eventDate)
val startTime = lessonList.firstOrNull { it.subjectId == subjectId }?.startTime
val type = when (exam.getString("Type")) {
"Praca klasowa",
"Sprawdzian" -> Event.TYPE_EXAM
"Kartkówka" -> Event.TYPE_SHORT_QUIZ
else -> Event.TYPE_DEFAULT
}
val eventObject = Event(
profileId = profileId,
id = id,
date = eventDate,
time = startTime,
topic = topic,
color = null,
type = type,
teacherId = teacherId,
subjectId = subjectId,
teamId = teamId
)
data.eventList.add(eventObject)
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_EVENT,
id,
profile?.empty ?: true,
profile?.empty ?: true
)
)
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_EXAMS, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_HEBE_EXAMS)
}
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-22.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import pl.szczodrzynski.edziennik.DAY
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_GRADE_SUMMARY
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.Utils
class VulcanHebeGradeSummary(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeGradeSummary"
}
init {
val entries = mapOf(
"Entry_1" to
if (data.studentSemesterNumber == 1)
Grade.TYPE_SEMESTER1_PROPOSED
else Grade.TYPE_SEMESTER2_PROPOSED,
"Entry_2" to
if (data.studentSemesterNumber == 1)
Grade.TYPE_SEMESTER1_FINAL
else Grade.TYPE_SEMESTER2_FINAL
)
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_GRADE_SUMMARY,
HebeFilterType.BY_PUPIL,
lastSync = lastSync
) { list, _ ->
list.forEach { grade ->
val subjectId = getSubjectId(grade, "Subject") ?: return@forEach
val addedDate = getDateTime(grade, "DateModify")
entries.onEach { (key, type) ->
val id = subjectId * -100 - type
val entry = grade.getString(key) ?: return@onEach
val value = Utils.getGradeValue(entry)
val color = Utils.getVulcanGradeColor(entry)
val gradeObject = Grade(
profileId = profileId,
id = id,
name = entry,
type = type,
value = value,
weight = 0f,
color = color,
category = "",
description = null,
comment = null,
semester = data.studentSemesterNumber,
teacherId = -1,
subjectId = subjectId,
addedDate = addedDate
)
data.gradeList.add(gradeObject)
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_GRADE,
id,
profile?.empty ?: true,
profile?.empty ?: true
)
)
}
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY, 1 * DAY)
onSuccess(ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY)
}
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-20.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_GRADES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_GRADES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import java.text.DecimalFormat
import kotlin.math.roundToInt
class VulcanHebeGrades(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeGrades"
}
init {
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_GRADES,
HebeFilterType.BY_PUPIL,
lastSync = lastSync
) { list, _ ->
list.forEach { grade ->
val id = grade.getLong("Id") ?: return@forEach
val column = grade.getJsonObject("Column")
val category = column.getJsonObject("Category")
val categoryText = category.getString("Name")
val teacherId = getTeacherId(grade, "Creator") ?: -1
val subjectId = getSubjectId(column, "Subject") ?: -1
val description = column.getString("Name")
val comment = grade.getString("Comment")
var value = grade.getFloat("Value")
var weight = column.getFloat("Weight") ?: 0.0f
val numerator = grade.getFloat("Numerator ")
val denominator = grade.getFloat("Denominator")
val addedDate = getDateTime(grade, "DateModify")
var finalDescription = ""
var name = when (numerator != null && denominator != null) {
true -> {
value = numerator / denominator
finalDescription += DecimalFormat("#.##").format(numerator) +
"/" + DecimalFormat("#.##").format(denominator)
weight = 0.0f
(value * 100).roundToInt().toString() + "%"
}
else -> {
if (value == null) weight = 0.0f
grade.getString("Content") ?: ""
}
}
comment?.also {
if (name == "") name = it
else finalDescription = (if (finalDescription == "") "" else " ") + it
}
description?.also {
finalDescription = (if (finalDescription == "") "" else " - ") + it
}
val columnColor = column.getInt("Color") ?: 0
val color = if (columnColor == 0)
when (name) {
"1-", "1", "1+" -> 0xffd65757
"2-", "2", "2+" -> 0xff9071b3
"3-", "3", "3+" -> 0xffd2ab24
"4-", "4", "4+" -> 0xff50b6d6
"5-", "5", "5+" -> 0xff2cbd92
"6-", "6", "6+" -> 0xff91b43c
else -> 0xff3D5F9C
}.toInt()
else
columnColor
val gradeObject = Grade(
profileId = profileId,
id = id,
name = name,
type = Grade.TYPE_NORMAL,
value = value ?: 0.0f,
weight = weight,
color = color,
category = categoryText,
description = finalDescription,
comment = null,
semester = getSemester(column),
teacherId = teacherId,
subjectId = subjectId,
addedDate = addedDate
)
data.gradeList.add(gradeObject)
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_GRADE,
id,
profile?.empty ?: true,
profile?.empty ?: true
)
)
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_GRADES, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_HEBE_GRADES)
}
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_HOMEWORK
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_HOMEWORK
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.getLong
import pl.szczodrzynski.edziennik.getString
class VulcanHebeHomework(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeHomework"
}
init {
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_HOMEWORK,
HebeFilterType.BY_PUPIL,
lastSync = lastSync
) { list, _ ->
list.forEach { exam ->
val id = exam.getLong("IdHomework") ?: return@forEach
val eventDate = getDate(exam, "Deadline") ?: return@forEach
val subjectId = getSubjectId(exam, "Subject") ?: -1
val teacherId = getTeacherId(exam, "Creator") ?: -1
val teamId = data.teamClass?.id ?: -1
val topic = exam.getString("Content")?.trim() ?: ""
if (!isCurrentYear(eventDate)) return@forEach
val lessonList = data.db.timetableDao().getAllForDateNow(profileId, eventDate)
val startTime = lessonList.firstOrNull { it.subjectId == subjectId }?.startTime
val eventObject = Event(
profileId = profileId,
id = id,
date = eventDate,
time = startTime,
topic = topic,
color = null,
type = Event.TYPE_HOMEWORK,
teacherId = teacherId,
subjectId = subjectId,
teamId = teamId
)
data.eventList.add(eventObject)
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_HOMEWORK,
id,
profile?.empty ?: true,
profile?.empty ?: true
)
)
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_HOMEWORK, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_HEBE_HOMEWORK)
}
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-22.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_LUCKY_NUMBER
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.LuckyNumber
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.getInt
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
class VulcanHebeLuckyNumber(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeLuckyNumber"
}
init {
apiGet(
TAG,
VULCAN_HEBE_ENDPOINT_LUCKY_NUMBER,
query = mapOf(
"constituentId" to data.studentConstituentId.toString(),
"day" to Date.getToday().stringY_m_d
)
) { lucky: JsonObject?, _ ->
// sync tomorrow if lucky number set or is weekend or afternoon
var nextSync = Date.getToday().stepForward(0, 0, 1).inMillis
if (lucky == null) {
if (Date.getToday().weekDay <= Week.FRIDAY && Time.getNow().hour < 12) {
// working days morning, sync always
nextSync = SYNC_ALWAYS
}
}
else {
val luckyNumberDate = Date.fromY_m_d(lucky.getString("Day")) ?: Date.getToday()
val luckyNumber = lucky.getInt("Number") ?: -1
val luckyNumberObject = LuckyNumber(
profileId = profileId,
date = luckyNumberDate,
number = luckyNumber
)
data.luckyNumberList.add(luckyNumberObject)
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_LUCKY_NUMBER,
luckyNumberObject.date.value.toLong(),
true,
profile?.empty ?: false
)
)
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER, syncAt = nextSync)
onSuccess(ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER)
}
}
}

View File

@ -0,0 +1,164 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-20.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonArray
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_VULCAN
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MAIN
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MAIN
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.utils.models.Date
class VulcanHebeMain(
override val data: DataVulcan,
override val lastSync: Long? = null
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeMain"
}
fun getStudents(
profile: Profile?,
profileList: MutableList<Profile>?,
loginStoreId: Int? = null,
firstProfileId: Int? = null,
onEmpty: (() -> Unit)? = null,
onSuccess: () -> Unit
) {
if (profile == null && (profileList == null || loginStoreId == null || firstProfileId == null))
throw IllegalArgumentException()
apiGet(
TAG,
VULCAN_HEBE_ENDPOINT_MAIN,
query = mapOf("lastSyncDate" to "null"),
baseUrl = profile == null
) { students: JsonArray, _ ->
if (students.isEmpty()) {
if (onEmpty != null)
onEmpty()
else
onSuccess()
return@apiGet
}
// safe to assume this will be non-null when creating a profile
var profileId = firstProfileId ?: loginStoreId ?: 1
students.forEach { studentEl ->
val student = studentEl.asJsonObject
val pupil = student.getJsonObject("Pupil")
val studentId = pupil.getInt("Id") ?: return@forEach
// check the student ID in case of not first login
if (profile != null && data.studentId != studentId)
return@forEach
val unit = student.getJsonObject("Unit")
val constituentUnit = student.getJsonObject("ConstituentUnit")
val login = student.getJsonObject("Login")
val periods = student.getJsonArray("Periods")?.map {
it.asJsonObject
} ?: listOf()
val period = periods.firstOrNull {
it.getBoolean("Current", false)
} ?: return@forEach
val periodLevel = period.getInt("Level") ?: return@forEach
val semester1 = periods.firstOrNull {
it.getInt("Level") == periodLevel && it.getInt("Number") == 1
}
val semester2 = periods.firstOrNull {
it.getInt("Level") == periodLevel && it.getInt("Number") == 2
}
val schoolSymbol = unit.getString("Symbol") ?: return@forEach
val schoolShort = constituentUnit.getString("Short") ?: return@forEach
val schoolCode = "${data.symbol}_$schoolSymbol"
val studentUnitId = unit.getInt("Id") ?: return@forEach
val studentConstituentId = constituentUnit.getInt("Id") ?: return@forEach
val studentLoginId = login.getInt("Id") ?: return@forEach
//val studentClassId = student.getInt("IdOddzial") ?: return@forEach
val studentClassName = student.getString("ClassDisplay") ?: return@forEach
val studentFirstName = pupil.getString("FirstName") ?: ""
val studentLastName = pupil.getString("Surname") ?: ""
val studentNameLong = "$studentFirstName $studentLastName".fixName()
val studentNameShort = "$studentFirstName ${studentLastName[0]}.".fixName()
val userLogin = login.getString("Value") ?: ""
val studentSemesterId = period.getInt("Id") ?: return@forEach
val studentSemesterNumber = period.getInt("Number") ?: return@forEach
val hebeContext = student.getString("Context")
val isParent = login.getString("LoginRole").equals("opiekun", ignoreCase = true)
val accountName = if (isParent)
login.getString("DisplayName")?.fixName()
else null
val dateSemester1Start = semester1
?.getJsonObject("Start")
?.getString("Date")
?.let { Date.fromY_m_d(it) }
val dateSemester2Start = semester2
?.getJsonObject("Start")
?.getString("Date")
?.let { Date.fromY_m_d(it) }
val dateYearEnd = semester2
?.getJsonObject("End")
?.getString("Date")
?.let { Date.fromY_m_d(it) }
val newProfile = profile ?: Profile(
profileId++,
loginStoreId!!,
LOGIN_TYPE_VULCAN,
studentNameLong,
userLogin,
studentNameLong,
studentNameShort,
accountName
)
newProfile.apply {
this.studentClassName = studentClassName
studentData["symbol"] = data.symbol
studentData["studentId"] = studentId
studentData["studentUnitId"] = studentUnitId
studentData["studentConstituentId"] = studentConstituentId
studentData["studentLoginId"] = studentLoginId
studentData["studentSemesterId"] = studentSemesterId
studentData["studentSemesterNumber"] = studentSemesterNumber
studentData["semester1Id"] = semester1?.getInt("Id") ?: 0
studentData["semester2Id"] = semester2?.getInt("Id") ?: 0
studentData["schoolSymbol"] = schoolSymbol
studentData["schoolShort"] = schoolShort
studentData["schoolName"] = schoolCode
studentData["hebeContext"] = hebeContext
}
dateSemester1Start?.let {
newProfile.dateSemester1Start = it
newProfile.studentSchoolYearStart = it.year
}
dateSemester2Start?.let { newProfile.dateSemester2Start = it }
dateYearEnd?.let { newProfile.dateYearEnd = it }
if (profile != null)
data.setSyncNext(ENDPOINT_VULCAN_HEBE_MAIN, 1 * DAY)
profileList?.add(newProfile)
}
onSuccess()
}
}
}

View File

@ -0,0 +1,133 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import androidx.core.util.set
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGES_SENT
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.*
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_DELETED
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT
import pl.szczodrzynski.navlib.crc16
import kotlin.text.replace
class VulcanHebeMessages(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeMessagesInbox"
}
private fun getPersonId(json: JsonObject): Long {
val senderLoginId = json.getInt("LoginId") ?: return -1
/*if (senderLoginId == data.studentLoginId)
return -1*/
val senderName = json.getString("Address") ?: return -1
val senderNameSplit = senderName.splitName()
val senderLoginIdStr = senderLoginId.toString()
val teacher = data.teacherList.singleOrNull { it.loginId == senderLoginIdStr }
?: Teacher(
profileId,
-1 * crc16(senderName).toLong(),
senderNameSplit?.second ?: "",
senderNameSplit?.first ?: "",
senderLoginIdStr
).also {
it.setTeacherType(Teacher.TYPE_OTHER)
data.teacherList[it.id] = it
}
return teacher.id
}
fun getMessages(messageType: Int) {
val folder = when (messageType) {
TYPE_RECEIVED -> 1
TYPE_SENT -> 2
TYPE_DELETED -> 3
else -> 1
}
val endpointId = when (messageType) {
TYPE_RECEIVED -> ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX
TYPE_SENT -> ENDPOINT_VULCAN_HEBE_MESSAGES_SENT
else -> ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX
}
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_MESSAGES,
HebeFilterType.BY_PERSON,
folder = folder,
lastSync = lastSync
) { list, _ ->
list.forEach { message ->
val id = message.getLong("Id") ?: return@forEach
val subject = message.getString("Subject") ?: return@forEach
val body = message.getString("Content") ?: return@forEach
val sender = message.getJsonObject("Sender") ?: return@forEach
val sentDate = getDateTime(message, "DateSent")
val readDate = getDateTime(message, "DateRead", default = 0)
if (!isCurrentYear(sentDate)) return@forEach
val messageObject = Message(
profileId = profileId,
id = id,
type = messageType,
subject = subject,
body = body.replace("\n", "<br>"),
senderId = if (messageType == TYPE_RECEIVED) getPersonId(sender) else null,
addedDate = sentDate
)
val receivers = message.getJsonArray("Receiver")
?.asJsonObjectList()
?: return@forEach
val receiverReadDate =
if (receivers.size == 1) readDate
else -1
for (receiver in receivers) {
val messageRecipientObject = MessageRecipient(
profileId,
if (messageType == TYPE_SENT) getPersonId(receiver) else -1,
-1,
receiverReadDate,
id
)
data.messageRecipientList.add(messageRecipientObject)
}
data.messageList.add(messageObject)
data.setSeenMetadataList.add(
Metadata(
profileId,
Metadata.TYPE_MESSAGE,
id,
readDate > 0 || messageType == TYPE_SENT,
readDate > 0 || messageType == TYPE_SENT
)
)
}
data.setSyncNext(
endpointId,
if (messageType == TYPE_RECEIVED) SYNC_ALWAYS else 1 * DAY,
if (messageType == TYPE_RECEIVED) null else DRAWER_ITEM_MESSAGES
)
onSuccess(endpointId)
}
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.JsonObject
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
class VulcanHebeMessagesChangeStatus(
override val data: DataVulcan,
private val messageObject: MessageFull,
val onSuccess: () -> Unit
) : VulcanHebe(data, null) {
companion object {
const val TAG = "VulcanHebeMessagesChangeStatus"
}
init {
apiPost(
TAG,
VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS,
payload = JsonObject(
"MessageId" to messageObject.id,
"LoginId" to data.studentLoginId,
"Status" to 1
)
) { _: Boolean, _ ->
if (!messageObject.seen) {
data.setSeenMetadataList.add(
Metadata(
profileId,
Metadata.TYPE_MESSAGE,
messageObject.id,
true,
true
)
)
messageObject.seen = true
}
if (messageObject.type != Message.TYPE_SENT) {
val messageRecipientObject = MessageRecipient(
profileId,
-1,
-1,
System.currentTimeMillis(),
messageObject.id
)
data.messageRecipientList.add(messageRecipientObject)
}
EventBus.getDefault().postSticky(MessageGetEvent(messageObject))
onSuccess()
}
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2021-2-22
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_NOTICES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_NOTICES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Notice
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
class VulcanHebeNotices(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeNotices"
}
init {
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_NOTICES,
HebeFilterType.BY_PUPIL,
lastSync = lastSync
) { list, _ ->
list.forEach { notice ->
val id = notice.getLong("Id") ?: return@forEach
val type = when (notice.getBoolean("Positive")) {
true -> Notice.TYPE_POSITIVE
else -> Notice.TYPE_NEUTRAL
}
val date = getDate(notice, "DateValid") ?: return@forEach
val semester = profile?.dateToSemester(date) ?: return@forEach
val text = notice.getString("Content") ?: ""
val category = notice.getJsonObject("Category")?.getString("Name")
val points = notice.getFloat("Points")
val teacherId = getTeacherId(notice, "Creator") ?: -1
val addedDate = getDateTime(notice, "DateModify")
if (!isCurrentYear(date)) return@forEach
val noticeObject = Notice(
profileId = profileId,
id = id,
type = type,
semester = semester,
text = text,
category = category,
points = points,
teacherId = teacherId,
addedDate = addedDate
)
data.noticeList.add(noticeObject)
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_NOTICE,
id,
profile?.empty ?: true,
profile?.empty ?: true
)
)
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_NOTICES, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_HEBE_NOTICES)
}
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-22.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonPrimitive
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_PUSH_ALL
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_PUSH_CONFIG
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
class VulcanHebePushConfig(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebePushConfig"
}
init {
apiPost(
TAG,
VULCAN_HEBE_ENDPOINT_PUSH_ALL,
payload = JsonPrimitive("on")
) { _: Boolean, _ ->
// sync always: this endpoint has .shouldSync set
data.setSyncNext(ENDPOINT_VULCAN_HEBE_PUSH_CONFIG, SYNC_ALWAYS)
data.app.config.sync.tokenVulcanList =
data.app.config.sync.tokenVulcanList + profileId
onSuccess(ENDPOINT_VULCAN_HEBE_PUSH_CONFIG)
}
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-22.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonObject
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_SEND
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
class VulcanHebeSendMessage(
override val data: DataVulcan,
val recipients: List<Teacher>,
val subject: String,
val text: String,
val onSuccess: () -> Unit
) : VulcanHebe(data, null) {
companion object {
const val TAG = "VulcanHebeSendMessage"
}
init {
val recipientsArray = JsonArray()
recipients.forEach { teacher ->
recipientsArray += JsonObject(
"Address" to teacher.fullNameLastFirst,
"LoginId" to (teacher.loginId?.toIntOrNull() ?: return@forEach),
"Initials" to teacher.initialsLastFirst,
"AddressHash" to teacher.fullNameLastFirst.sha1Hex()
)
}
val senderName = (profile?.accountName ?: profile?.studentNameLong)
?.swapFirstLastName() ?: ""
val sender = JsonObject(
"Address" to senderName,
"LoginId" to data.studentLoginId.toString(),
"Initials" to senderName.getNameInitials(),
"AddressHash" to senderName.sha1Hex()
)
apiPost(
TAG,
VULCAN_HEBE_ENDPOINT_MESSAGES_SEND,
payload = JsonObject(
"Status" to 1,
"Sender" to sender,
"DateSent" to null,
"DateRead" to null,
"Content" to text,
"Receiver" to recipientsArray,
"Id" to 0,
"Subject" to subject,
"Attachments" to null,
"Self" to null
)
) { json: JsonObject, _ ->
val messageId = json.getLong("Id")
if (messageId == null) {
// TODO error
return@apiPost
}
VulcanHebeMessages(data, null) {
val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject }
val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == messageId }
val event = MessageSentEvent(data.profileId, message, message?.addedDate)
EventBus.getDefault().postSticky(event)
onSuccess()
}.getMessages(Message.TYPE_SENT)
}
}
}

View File

@ -0,0 +1,251 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_TIMETABLE
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_TIMETABLE
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CANCELLED
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CHANGE
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_NORMAL
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_SHIFTED_SOURCE
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_SHIFTED_TARGET
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Week
class VulcanHebeTimetable(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeTimetable"
}
private val lessonList = mutableListOf<Lesson>()
private val lessonDates = mutableSetOf<Int>()
init {
val previousWeekStart = Week.getWeekStart().stepForward(0, 0, -7)
if (Date.getToday().weekDay > 4) {
previousWeekStart.stepForward(0, 0, 7)
}
val dateFrom = data.arguments
?.getString("weekStart")
?.let { Date.fromY_m_d(it) }
?: previousWeekStart
val dateTo = dateFrom.clone().stepForward(0, 0, 13)
val lastSync = null
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_TIMETABLE,
HebeFilterType.BY_PUPIL,
dateFrom = dateFrom,
dateTo = dateTo,
lastSync = lastSync
) { lessons, _ ->
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGES,
HebeFilterType.BY_PUPIL,
dateFrom = dateFrom,
dateTo = dateTo,
lastSync = lastSync
) { changes, _ ->
processData(lessons, changes)
// cancel lesson changes when caused by a shift
for (lesson in lessonList) {
if (lesson.type != TYPE_SHIFTED_TARGET)
continue
lessonList.firstOrNull {
it.oldDate == lesson.date
&& it.oldLessonNumber == lesson.lessonNumber
&& it.type == TYPE_CHANGE
}?.let {
it.type = TYPE_CANCELLED
it.date = null
it.lessonNumber = null
it.startTime = null
it.endTime = null
it.subjectId = null
it.teacherId = null
it.teamId = null
it.classroom = null
}
}
// add TYPE_NO_LESSONS to empty dates
val date: Date = dateFrom.clone()
while (date <= dateTo) {
if (!lessonDates.contains(date.value)) {
lessonList.add(Lesson(profileId, date.value.toLong()).apply {
this.type = Lesson.TYPE_NO_LESSONS
this.date = date.clone()
})
}
date.stepForward(0, 0, 1)
}
d(
TAG,
"Clearing lessons between ${dateFrom.stringY_m_d} and ${dateTo.stringY_m_d}"
)
data.lessonList.addAll(lessonList)
data.setSyncNext(ENDPOINT_VULCAN_HEBE_TIMETABLE, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_HEBE_TIMETABLE)
}
}
}
private fun buildLesson(changes: List<JsonObject>, json: JsonObject): Pair<Lesson, Lesson?>? {
val lesson = Lesson(profileId, -1)
var lessonShift: Lesson? = null
val lessonDate = getDate(json, "Date") ?: return null
val lessonRange = getLessonRange(json, "TimeSlot")
val startTime = lessonRange?.startTime
val endTime = lessonRange?.endTime
val teacherId = getTeacherId(json, "TeacherPrimary")
val classroom = json.getJsonObject("Room").getString("Code")
val subjectId = getSubjectId(json, "Subject")
val teamId = getTeamId(json, "Distribution")
?: getClassId(json, "Clazz")
?: data.teamClass?.id
?: -1
val change = json.getJsonObject("Change")
val changeId = change.getInt("Id")
val type = when (change.getInt("Type")) {
1 -> TYPE_CANCELLED
2 -> TYPE_CHANGE
3 -> TYPE_SHIFTED_SOURCE
4 -> TYPE_CANCELLED // TODO: 2021-02-21 add showing cancellation reason
else -> TYPE_NORMAL
}
lesson.type = type
if (type == TYPE_NORMAL) {
lesson.date = lessonDate
lesson.lessonNumber = lessonRange?.lessonNumber
lesson.startTime = startTime
lesson.endTime = endTime
lesson.subjectId = subjectId
lesson.teacherId = teacherId
lesson.teamId = teamId
lesson.classroom = classroom
} else {
lesson.oldDate = lessonDate
lesson.oldLessonNumber = lessonRange?.lessonNumber
lesson.oldStartTime = startTime
lesson.oldEndTime = endTime
lesson.oldSubjectId = subjectId
lesson.oldTeacherId = teacherId
lesson.oldTeamId = teamId
lesson.oldClassroom = classroom
}
if (type == TYPE_CHANGE || type == TYPE_SHIFTED_SOURCE) {
val changeJson = changes.firstOrNull {
it.getInt("Id") == changeId
} ?: return lesson to null
val changeLessonDate = getDate(changeJson, "LessonDate") ?: return lesson to null
val changeLessonRange = getLessonRange(changeJson, "TimeSlot") ?: lessonRange
val changeStartTime = changeLessonRange?.startTime
val changeEndTime = changeLessonRange?.endTime
val changeTeacherId = getTeacherId(changeJson, "TeacherPrimary") ?: teacherId
val changeClassroom = changeJson.getJsonObject("Room").getString("Code") ?: classroom
val changeSubjectId = getSubjectId(changeJson, "Subject") ?: subjectId
val changeTeamId = getTeamId(json, "Distribution")
?: getClassId(json, "Clazz")
?: teamId
if (type != TYPE_CHANGE) {
/* lesson shifted */
lessonShift = Lesson(profileId, -1)
lessonShift.type = TYPE_SHIFTED_TARGET
// update source lesson with the target lesson date
lesson.date = changeLessonDate
lesson.lessonNumber = changeLessonRange?.lessonNumber
lesson.startTime = changeStartTime
lesson.endTime = changeEndTime
// update target lesson with the source lesson date
lessonShift.oldDate = lessonDate
lessonShift.oldLessonNumber = lessonRange?.lessonNumber
lessonShift.oldStartTime = startTime
lessonShift.oldEndTime = endTime
}
(if (type == TYPE_CHANGE) lesson else lessonShift)
?.apply {
this.date = changeLessonDate
this.lessonNumber = changeLessonRange?.lessonNumber
this.startTime = changeStartTime
this.endTime = changeEndTime
this.subjectId = changeSubjectId
this.teacherId = changeTeacherId
this.teamId = changeTeamId
this.classroom = changeClassroom
}
}
return lesson to lessonShift
}
private fun processData(lessons: List<JsonObject>, changes: List<JsonObject>) {
lessons.forEach { lessonJson ->
if (lessonJson.getBoolean("Visible") != true)
return@forEach
val lessonPair = buildLesson(changes, lessonJson) ?: return@forEach
val (lessonObject, lessonShift) = lessonPair
when {
lessonShift != null -> lessonShift
lessonObject.type != TYPE_NORMAL -> lessonObject
else -> null
}?.let { lesson ->
val lessonDate = lesson.displayDate ?: return@let
val seen = profile?.empty ?: true || lessonDate < Date.getToday()
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_LESSON_CHANGE,
lesson.id,
seen,
seen
)
)
}
lessonObject.id = lessonObject.buildId()
lessonShift?.id = lessonShift?.buildId() ?: -1
lessonList.add(lessonObject)
lessonShift?.let { lessonList.add(it) }
lessonObject.displayDate?.let { lessonDates.add(it.value) }
lessonShift?.displayDate?.let { lessonDates.add(it.value) }
}
}
}

View File

@ -9,9 +9,12 @@ import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanWebMain
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.VulcanHebeMain
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.CufsCertificate
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginHebe
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain
import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -25,6 +28,7 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
private val api = VulcanApi(data, null)
private val web = VulcanWebMain(data, null)
private val hebe = VulcanHebe(data, null)
private val profileList = mutableListOf<Profile>()
private val loginStoreId = data.loginStore.id
private var firstProfileId = loginStoreId
@ -50,12 +54,18 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
checkSymbol(certificate)
}
}
else {
else if (data.loginStore.mode == LOGIN_MODE_VULCAN_API) {
registerDevice {
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore))
onSuccess()
}
}
else {
registerDeviceHebe {
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore))
onSuccess()
}
}
}
private fun checkSymbol(certificate: CufsCertificate) {
@ -103,7 +113,7 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
data.apiPin = data.apiPin.toMutableMap().also {
it[symbol] = json.getString("PIN")
}
registerDevice(onSuccess)
registerDeviceHebe(onSuccess)
}
}
}
@ -197,4 +207,21 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
}
}
}
private fun registerDeviceHebe(onSuccess: () -> Unit) {
VulcanLoginHebe(data) {
VulcanHebeMain(data).getStudents(
profile = null,
profileList,
loginStoreId,
firstProfileId,
onEmpty = {
EventBus.getDefault()
.postSticky(FirstLoginFinishedEvent(listOf(), data.loginStore))
onSuccess()
},
onSuccess = onSuccess
)
}
}
}

View File

@ -6,6 +6,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_WEB_MAIN
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.utils.Utils
@ -54,6 +55,10 @@ class VulcanLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_login_vulcan_api)
VulcanLoginApi(data) { onSuccess(loginMethodId) }
}
LOGIN_METHOD_VULCAN_HEBE -> {
data.startProgress(R.string.edziennik_progress_login_vulcan_api)
VulcanLoginHebe(data) { onSuccess(loginMethodId) }
}
}
}
}

View File

@ -191,18 +191,6 @@ class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) {
}
}
val deviceId = data.app.deviceId.padStart(16, '0')
val loginStoreId = data.loginStore.id.toString(16).padStart(4, '0')
val symbol = data.symbol?.crc16()?.toString(16)?.take(2) ?: "00"
val uuid =
deviceId.substring(0..7) +
"-" + deviceId.substring(8..11) +
"-" + deviceId.substring(12..15) +
"-" + loginStoreId +
"-" + symbol + "6f72616e7a"
val deviceNameSuffix = " - nie usuwać"
val szkolnyApi = SzkolnyApi(data.app)
val firebaseToken = szkolnyApi.runCatching({
getFirebaseToken("vulcan")
@ -216,8 +204,8 @@ class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) {
.addHeader("RequestMobileType", "RegisterDevice")
.addParameter("PIN", data.apiPin[data.symbol])
.addParameter("TokenKey", data.apiToken[data.symbol])
.addParameter("DeviceId", uuid)
.addParameter("DeviceName", VULCAN_API_DEVICE_NAME.take(50 - deviceNameSuffix.length) + deviceNameSuffix)
.addParameter("DeviceId", data.buildDeviceId())
.addParameter("DeviceName", VULCAN_API_DEVICE_NAME)
.addParameter("DeviceNameUser", "")
.addParameter("DeviceDescription", "")
.addParameter("DeviceSystemType", "Android")

View File

@ -0,0 +1,110 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-20.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login
import com.google.gson.JsonObject
import io.github.wulkanowy.signer.hebe.generateKeyPair
import pl.szczodrzynski.edziennik.JsonObject
import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_DATA_MISSING
import pl.szczodrzynski.edziennik.data.api.VULCAN_API_DEVICE_NAME
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_REGISTER_NEW
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
class VulcanLoginHebe(val data: DataVulcan, val onSuccess: () -> Unit) {
companion object {
private const val TAG = "VulcanLoginHebe"
}
init { run {
// i'm sure this does something useful
// not quite sure what, though
if (data.studentSemesterNumber == 1 && data.semester1Id == 0)
data.semester1Id = data.studentSemesterNumber
if (data.studentSemesterNumber == 2 && data.semester2Id == 0)
data.semester2Id = data.studentSemesterNumber
copyFromLoginStore()
if (data.profile != null && data.isApiLoginValid()) {
onSuccess()
}
else {
if (data.symbol.isNotNullNorEmpty() && data.apiToken[data.symbol].isNotNullNorEmpty() && data.apiPin[data.symbol].isNotNullNorEmpty()) {
loginWithToken()
}
else {
data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING))
}
}
}}
private fun copyFromLoginStore() {
data.loginStore.data.apply {
// map form inputs to the symbol
if (has("symbol")) {
data.symbol = getString("symbol")
remove("symbol")
}
if (has("deviceToken")) {
data.apiToken = data.apiToken.toMutableMap().also {
it[data.symbol] = getString("deviceToken")
}
remove("deviceToken")
}
if (has("devicePin")) {
data.apiPin = data.apiPin.toMutableMap().also {
it[data.symbol] = getString("devicePin")
}
remove("devicePin")
}
}
}
private fun loginWithToken() {
val szkolnyApi = SzkolnyApi(data.app)
val hebe = VulcanHebe(data, null)
if (data.hebePublicKey == null || data.hebePrivateKey == null || data.hebePublicHash == null) {
val (publicPem, privatePem, publicHash) = generateKeyPair()
data.hebePublicKey = publicPem
data.hebePrivateKey = privatePem
data.hebePublicHash = publicHash
}
val firebaseToken = szkolnyApi.runCatching({
getFirebaseToken("vulcan")
}, onError = {
// screw errors
}) ?: data.app.config.sync.tokenVulcan
hebe.apiPost(
TAG,
VULCAN_HEBE_ENDPOINT_REGISTER_NEW,
payload = JsonObject(
"OS" to "Android",
"PIN" to data.apiPin[data.symbol],
"Certificate" to data.hebePublicKey,
"CertificateType" to "RSA_PEM",
"DeviceModel" to VULCAN_API_DEVICE_NAME,
"SecurityToken" to data.apiToken[data.symbol],
"SelfIdentifier" to data.buildDeviceId(),
"CertificateThumbprint" to data.hebePublicHash
),
baseUrl = true,
firebaseToken = firebaseToken
) { _: JsonObject, _ ->
data.apiToken = data.apiToken.toMutableMap().also {
it[data.symbol] = it[data.symbol]?.substring(0, 3)
}
data.loginStore.removeLoginData("apiPin")
onSuccess()
}
}
}

View File

@ -89,7 +89,7 @@ class VulcanLoginWebMain(val data: DataVulcan, val onSuccess: () -> Unit) {
return true
}
val fsLogin = FSLogin(data.app.http, debug = App.debugMode)
val fsLogin = FSLogin(data.app.http, debug = App.devMode)
fsLogin.performLogin(
realm = realm,
username = data.webUsername ?: data.webEmail ?: return false,

View File

@ -136,7 +136,7 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
val db: AppDb by lazy { app.db }
init {
if (App.devMode) {
if (App.debugMode) {
fakeLogin = loginStore.hasLoginData("fakeLogin")
}
clear()

View File

@ -46,6 +46,6 @@ object Signing {
/*fun provideKey(param1: String, param2: Long): ByteArray {*/
fun pleaseStopRightNow(param1: String, param2: Long): ByteArray {
return "$param1.MTIzNDU2Nzg5MDSA64wjse===.$param2".sha256()
return "$param1.MTIzNDU2Nzg5MD8+8LPFtX===.$param2".sha256()
}
}

View File

@ -11,8 +11,8 @@ import pl.szczodrzynski.edziennik.currentTimeUnix
data class RegisterAvailabilityStatus(
val available: Boolean,
val name: String?,
val message: Message?,
val nextCheck: Long = currentTimeUnix() + 7 * DAY,
val userMessage: Message?,
val nextCheckAt: Long = currentTimeUnix() + 7 * DAY,
val minVersionCode: Int = BuildConfig.VERSION_CODE
) {
data class Message(

View File

@ -103,7 +103,9 @@ open class Profile(
dateSemester2Start.year -= diff
dateYearEnd.year -= diff
}
return Date.getToday() >= dateYearEnd && Date.getToday().year > studentSchoolYearStart
return App.config.archiverEnabled
&& Date.getToday() >= dateYearEnd
&& Date.getToday().year > studentSchoolYearStart
}
fun isBeforeYear() = false && Date.getToday() < dateSemester1Start

View File

@ -11,6 +11,7 @@ import androidx.room.Entity
import androidx.room.Ignore
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.fixName
import pl.szczodrzynski.edziennik.getNameInitials
import pl.szczodrzynski.edziennik.join
import java.util.*
@ -180,6 +181,9 @@ open class Teacher {
@delegate:Ignore
val fullNameLastFirst by lazy { "$surname $name".fixName() }
@delegate:Ignore
val initialsLastFirst by lazy { fullNameLastFirst.getNameInitials() }
val shortName: String
get() = (if (name == null || name?.length == 0) "" else name!![0].toString()) + "." + surname

View File

@ -29,6 +29,7 @@ class SzkolnyVulcanFirebase(val app: App, val profiles: List<Profile>, val messa
val data = message.data.getString("data")?.toJsonObject() ?: return@run
val type = data.getString("table") ?: return@run
val studentId = data.getInt("pupilid")
val loginId = data.getInt("loginid")
/* pl.vulcan.uonetmobile.auxilary.enums.CDCPushEnum */
val viewIdPair = when (type.toLowerCase(Locale.ROOT)) {
@ -42,8 +43,9 @@ class SzkolnyVulcanFirebase(val app: App, val profiles: List<Profile>, val messa
}
val tasks = profiles.filter {
it.loginStoreType == LOGIN_TYPE_VULCAN &&
it.getStudentData("studentId", 0) == studentId
it.loginStoreType == LOGIN_TYPE_VULCAN
&& (it.getStudentData("studentId", 0) == studentId
|| it.getStudentData("studentLoginId", 0) == loginId)
}.map {
EdziennikTask.syncProfile(it.id, listOf(viewIdPair))
}

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.ui.dialogs
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
@ -46,16 +47,17 @@ class RegisterUnavailableDialog(
onShowListener?.invoke(TAG)
app = activity.applicationContext as App
if (!status.available && status.message != null) {
if (!status.available && status.userMessage != null) {
val b = DialogRegisterUnavailableBinding.inflate(LayoutInflater.from(activity), null, false)
b.message = status.message
if (status.message.image != null)
b.image.load(status.message.image)
if (status.message.url != null) {
b.message = status.userMessage
if (status.userMessage.image != null)
b.image.load(status.userMessage.image)
if (status.userMessage.url != null) {
b.readMore.onClick {
Utils.openUrl(activity, status.message.url)
Utils.openUrl(activity, status.userMessage.url)
}
}
b.text.movementMethod = LinkMovementMethod.getInstance()
dialog = MaterialAlertDialogBuilder(activity)
.setView(b.root)
.setPositiveButton(R.string.close) { dialog, _ ->
@ -65,6 +67,7 @@ class RegisterUnavailableDialog(
onDismissListener?.invoke(TAG)
}
.show()
return@run
}
val update = app.config.update

View File

@ -193,7 +193,7 @@ class EventDetailsDialog(
b.goToTimetableButton.attachToastHint(R.string.hint_go_to_timetable)
// RE-DOWNLOAD
b.downloadButton.isVisible = App.debugMode
b.downloadButton.isVisible = App.devMode
b.downloadButton.onClick {
EdziennikTask.eventGet(event.profileId, event).enqueue(activity)
}

View File

@ -57,7 +57,7 @@ class GradeDetailsDialog(
b.grade = grade
b.weightText = manager.getWeightString(app, grade)
b.commentVisible = false
b.devMode = App.debugMode
b.devMode = App.devMode
b.gradeName.setTextColor(if (ColorUtils.calculateLuminance(gradeColor) > 0.3) 0xaa000000.toInt() else 0xccffffff.toInt())
b.gradeName.background.setTintColor(gradeColor)

View File

@ -78,7 +78,7 @@ class LessonDetailsDialog(
)
}
if (App.debugMode)
if (App.devMode)
b.lessonId.visibility = View.VISIBLE
update()

View File

@ -55,7 +55,7 @@ class AttendanceDetailsDialog(
val attendanceColor = manager.getAttendanceColor(attendance)
b.attendance = attendance
b.devMode = App.debugMode
b.devMode = App.devMode
b.attendanceName.setTextColor(if (ColorUtils.calculateLuminance(attendanceColor) > 0.3) 0xaa000000.toInt() else 0xccffffff.toInt())
b.attendanceName.background.setTintColor(attendanceColor)

View File

@ -167,7 +167,7 @@ class CrashActivity : AppCompatActivity(), CoroutineScope {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
clipboard?.apply {
val clip = ClipData.newPlainText(getString(R.string.customactivityoncrash_error_activity_error_details_clipboard_label), errorInformation)
primaryClip = clip
setPrimaryClip(clip)
Toast.makeText(this@CrashActivity, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
}
}

View File

@ -45,6 +45,8 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
}
override fun onPageCreated(): Boolean {
b.app = app
b.last10unseen.onClick {
launch(Dispatchers.Default) {
val events = app.db.eventDao().getAllNow(App.profileId)

View File

@ -45,7 +45,7 @@ class ErrorDetailsDialog(
listOf(
it.getStringReason(activity).asBoldSpannable().asColoredSpannable(R.attr.colorOnBackground.resolveAttr(activity)),
activity.getString(R.string.error_unknown_format, it.errorCode, it.tag),
if (App.debugMode)
if (App.devMode)
it.throwable?.stackTraceString ?: it.throwable?.localizedMessage
else
it.throwable?.localizedMessage

View File

@ -71,9 +71,14 @@ class GradesListFragment : Fragment(), CoroutineScope {
val adapter = GradesAdapter(activity)
var firstRun = true
app.db.gradeDao().getAllOrderBy(App.profileId, app.gradesManager.getOrderByString()).observe(this@GradesListFragment, Observer { items -> this@GradesListFragment.launch {
app.db.gradeDao().getAllOrderBy(App.profileId, app.gradesManager.getOrderByString()).observe(this@GradesListFragment, Observer { grades -> this@GradesListFragment.launch {
if (!isAdded) return@launch
val items = when {
app.config.forProfile().grades.hideSticksFromOld && App.devMode -> grades.filter { it.value != 1.0f }
else -> grades
}
// load & configure the adapter
adapter.items = withContext(Dispatchers.Default) { processGrades(items) }
if (items.isNotNullNorEmpty() && b.list.adapter == null) {

View File

@ -152,13 +152,14 @@ class HomeFragment : Fragment(), CoroutineScope {
val items = mutableListOf<HomeCard>()
cards.mapNotNullTo(items) {
@Suppress("USELESS_CAST")
when (it.cardId) {
HomeCard.CARD_LUCKY_NUMBER -> HomeLuckyNumberCard(it.cardId, app, activity, this, app.profile)
HomeCard.CARD_TIMETABLE -> HomeTimetableCard(it.cardId, app, activity, this, app.profile)
HomeCard.CARD_GRADES -> HomeGradesCard(it.cardId, app, activity, this, app.profile)
HomeCard.CARD_EVENTS -> HomeEventsCard(it.cardId, app, activity, this, app.profile)
else -> null
}
} as HomeCard?
}
//if (App.devMode)
// items += HomeDebugCard(100, app, activity, this, app.profile)

View File

@ -8,6 +8,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
import androidx.core.view.plusAssign
import androidx.core.view.setMargins
@ -57,13 +58,13 @@ class HomeAvailabilityCard(
var onInfoClick = { _: View -> }
if (status != null && !status.available && status.message != null) {
b.homeAvailabilityTitle.text = status.message.title
b.homeAvailabilityText.text = status.message.contentShort
if (status != null && !status.available && status.userMessage != null) {
b.homeAvailabilityTitle.text = HtmlCompat.fromHtml(status.userMessage.title, HtmlCompat.FROM_HTML_MODE_LEGACY)
b.homeAvailabilityText.text = HtmlCompat.fromHtml(status.userMessage.contentShort, HtmlCompat.FROM_HTML_MODE_LEGACY)
b.homeAvailabilityUpdate.isVisible = false
b.homeAvailabilityIcon.setImageResource(R.drawable.ic_sync)
if (status.message.icon != null)
b.homeAvailabilityIcon.load(status.message.icon)
if (status.userMessage.icon != null)
b.homeAvailabilityIcon.load(status.userMessage.icon)
onInfoClick = {
RegisterUnavailableDialog(activity, status)
}

View File

@ -25,7 +25,6 @@ import kotlin.coroutines.CoroutineContext
class LoginActivity : AppCompatActivity(), CoroutineScope {
companion object {
private const val TAG = "LoginActivity"
var thisOneIsTricky = 0
}
private val app: App by lazy { applicationContext as App }
@ -42,6 +41,8 @@ class LoginActivity : AppCompatActivity(), CoroutineScope {
val profiles = mutableListOf<LoginSummaryAdapter.Item>()
val loginStores = mutableListOf<LoginStore>()
fun getRootView() = b.root
override fun onBackPressed() {
val destination = nav.currentDestination ?: run {
nav.navigateUp()
@ -55,6 +56,11 @@ class LoginActivity : AppCompatActivity(), CoroutineScope {
return
if (destination.id == R.id.loginFinishFragment)
return
// eggs
if (destination.id == R.id.loginPrizeFragment) {
finish()
return
}
if (destination.id == R.id.loginChooserFragment && loginStores.isEmpty()) {
setResult(Activity.RESULT_CANCELED)
finish()
@ -79,8 +85,6 @@ class LoginActivity : AppCompatActivity(), CoroutineScope {
super.onCreate(savedInstanceState)
setTheme(R.style.AppTheme_Light)
thisOneIsTricky = -1
navOptions = NavOptions.Builder()
.setEnterAnim(R.anim.slide_in_right)
.setExitAnim(R.anim.slide_out_left)

View File

@ -60,7 +60,8 @@ class LoginChooserAdapter(
private val onClickListener = View.OnClickListener { view ->
val model = view.getTag(R.string.tag_key_model)
if (model is LoginInfo.Register && model.loginModes.size == 1) {
if (model is LoginInfo.Register
&& model.loginModes.count { App.devMode || !it.isDevOnly } == 1) {
onModeClick?.invoke(model, model.loginModes.first())
return@OnClickListener
}
@ -85,7 +86,9 @@ class LoginChooserAdapter(
if (model.state == STATE_CLOSED) {
val subItems = model.items
val subItems = model.items.filter {
App.devMode || !it.isDevOnly
}
model.state = STATE_OPENED
items.addAll(position + 1, subItems)

View File

@ -4,15 +4,22 @@
package pl.szczodrzynski.edziennik.ui.modules.login
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.Animation
import android.view.animation.RotateAnimation
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
@ -26,6 +33,8 @@ import kotlin.coroutines.CoroutineContext
class LoginChooserFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "LoginChooserFragment"
// eggs
var isRotated = false
}
private lateinit var app: App
@ -47,31 +56,33 @@ class LoginChooserFragment : Fragment(), CoroutineScope {
return b.root
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (!isAdded) return
val adapter = LoginChooserAdapter(activity) { loginType, loginMode ->
launch {
if (!checkAvailability(loginType.loginType))
return@launch
if (loginMode.isPlatformSelection) {
nav.navigate(R.id.loginPlatformListFragment, Bundle(
"loginType" to loginType.loginType,
"loginMode" to loginMode.loginMode
), activity.navOptions)
return@launch
}
nav.navigate(R.id.loginFormFragment, Bundle(
"loginType" to loginType.loginType,
"loginMode" to loginMode.loginMode
), activity.navOptions)
}
}
val adapter = LoginChooserAdapter(activity, this::onLoginModeClicked)
LoginInfo.chooserList = LoginInfo.chooserList
?: LoginInfo.list.toMutableList<Any>()
?: LoginInfo.list.toMutableList()
// eggs
if (isRotated) {
isRotated = false
LoginFormFragment.wantEggs = false
LoginInfo.chooserList = LoginInfo.list.toMutableList()
val anim = RotateAnimation(
180f,
0f,
Animation.RELATIVE_TO_SELF,
0.5f,
Animation.RELATIVE_TO_SELF,
0.5f
)
anim.interpolator = AccelerateDecelerateInterpolator()
anim.duration = 500
anim.fillAfter = true
activity.getRootView().startAnimation(anim)
}
adapter.items = LoginInfo.chooserList!!
b.list.adapter = adapter
@ -85,6 +96,81 @@ class LoginChooserFragment : Fragment(), CoroutineScope {
startActivity(Intent(activity, FeedbackActivity::class.java))
}
// eggs
b.footnoteText.onClick {
if (!LoginFormFragment.wantEggs || isRotated)
return@onClick
val text = b.subtitleText.text.toString()
if (text.endsWith(".."))
b.subtitleText.text = text.substring(0, text.length - 2)
else
b.subtitleText.text = "$text..."
}
var clickCount = 0
val color = R.color.md_blue_500.resolveColor(app)
val hsv = FloatArray(3)
Color.colorToHSV(color, hsv)
val hueOriginal = hsv[0]
b.subtitleText.onClick {
if (isRotated)
return@onClick
val text = b.subtitleText.text.toString()
if (text.endsWith("..") && !text.endsWith("...")) {
clickCount++
}
if (clickCount == 5) {
val anim = ValueAnimator.ofFloat(0f, 1f)
anim.duration = 5000
anim.addUpdateListener {
hsv[0] = hueOriginal + it.animatedFraction * 3f * 360f
hsv[0] = hsv[0] % 360f
b.topLogo.drawable.setTintColor(Color.HSVToColor(Color.alpha(color), hsv))
}
anim.start()
}
}
b.topLogo.onClick {
if (clickCount != 5 || isRotated) {
clickCount = 0
return@onClick
}
isRotated = true
val anim = RotateAnimation(
0f,
180f,
Animation.RELATIVE_TO_SELF,
0.5f,
Animation.RELATIVE_TO_SELF,
0.5f
)
anim.interpolator = AccelerateDecelerateInterpolator()
anim.duration = 2000
anim.fillAfter = true
activity.getRootView().startAnimation(anim)
b.list.smoothScrollToPosition(0)
adapter.items.add(
LoginInfo.Register(
loginType = 74,
internalName = "eggs",
registerName = R.string.eggs,
registerLogo = R.drawable.face_1,
loginModes = listOf(
LoginInfo.Mode(
loginMode = 0,
name = 0,
icon = 0,
guideText = 0,
credentials = listOf(),
errorCodes = mapOf()
)
)
)
)
adapter.notifyItemInserted(adapter.items.size - 1)
}
when {
activity.loginStores.isNotEmpty() -> {
// we are navigated here from LoginSummary
@ -106,6 +192,50 @@ class LoginChooserFragment : Fragment(), CoroutineScope {
}
}
private fun onLoginModeClicked(
loginType: LoginInfo.Register,
loginMode: LoginInfo.Mode
) {
if (loginType.internalName == "eggs") {
nav.navigate(R.id.loginEggsFragment, null, activity.navOptions)
return
}
launch {
if (!checkAvailability(loginType.loginType))
return@launch
if (loginMode.isTesting || loginMode.isDevOnly) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.login_chooser_testing_title)
.setMessage(R.string.login_chooser_testing_text)
.setPositiveButton(R.string.ok) { _, _ ->
navigateToLoginMode(loginType, loginMode)
}
.setNegativeButton(R.string.cancel, null)
.show()
return@launch
}
navigateToLoginMode(loginType, loginMode)
}
}
private fun navigateToLoginMode(loginType: LoginInfo.Register, loginMode: LoginInfo.Mode) {
if (loginMode.isPlatformSelection) {
nav.navigate(R.id.loginPlatformListFragment, Bundle(
"loginType" to loginType.loginType,
"loginMode" to loginMode.loginMode
), activity.navOptions)
return
}
nav.navigate(R.id.loginFormFragment, Bundle(
"loginType" to loginType.loginType,
"loginMode" to loginMode.loginMode
), activity.navOptions)
}
private suspend fun checkAvailability(loginType: Int): Boolean {
when (loginType) {
LOGIN_TYPE_LIBRUS -> "librus"
@ -117,7 +247,7 @@ class LoginChooserFragment : Fragment(), CoroutineScope {
else -> null
}?.let { registerName ->
var status = app.config.sync.registerAvailability[registerName]
if (status == null || status.nextCheck < currentTimeUnix()) {
if (status == null || status.nextCheckAt < currentTimeUnix()) {
withContext(Dispatchers.IO) {
val api = SzkolnyApi(app)
api.runCatching(activity) {

View File

@ -0,0 +1,116 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-10-18.
*/
package pl.szczodrzynski.edziennik.ui.modules.login
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.Animation
import android.view.animation.RotateAnimation
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.md5
import kotlin.coroutines.CoroutineContext
class LoginEggsFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "LoginEggsFragment"
}
private lateinit var app: App
private lateinit var activity: LoginActivity
private lateinit var view: ViewGroup
private lateinit var webView: WebView
private val nav by lazy { activity.nav }
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local/private variables go here
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as LoginActivity?) ?: return null
context ?: return null
app = activity.application as App
webView = WebView(activity)
view = FrameLayout(activity)
view.addView(webView)
return view
}
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (!isAdded) return
if (!LoginChooserFragment.isRotated) {
nav.navigateUp()
return
}
val anim = RotateAnimation(
180f,
0f,
Animation.RELATIVE_TO_SELF,
0.5f,
Animation.RELATIVE_TO_SELF,
0.5f
)
anim.interpolator = AccelerateDecelerateInterpolator()
anim.duration = 10
anim.fillAfter = true
activity.getRootView().startAnimation(anim)
webView.apply {
settings.apply {
javaScriptEnabled = true
}
addJavascriptInterface(object : Any() {
@Suppress("NAME_SHADOWING")
@JavascriptInterface
fun getPrize() {
val anim = RotateAnimation(
0f,
180f,
Animation.RELATIVE_TO_SELF,
0.5f,
Animation.RELATIVE_TO_SELF,
0.5f
)
anim.interpolator = AccelerateDecelerateInterpolator()
anim.duration = 10
anim.fillAfter = true
activity.getRootView().startAnimation(anim)
nav.navigate(R.id.loginPrizeFragment, null, activity.navOptions)
}
}, "EggInterface")
loadUrl("https://szkolny.eu/game/runner.html")
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
val deviceId = app.deviceId.md5()
val version = BuildConfig.VERSION_NAME
val js = """initPage("$deviceId", true, "$version");"""
webView.evaluateJavascript(js) {}
}
}
}
}
}
}

View File

@ -22,8 +22,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.databinding.LoginFormCheckboxItemBinding
import pl.szczodrzynski.edziennik.databinding.LoginFormFieldItemBinding
import pl.szczodrzynski.edziennik.databinding.LoginFormFragmentBinding
import pl.szczodrzynski.edziennik.databinding.LoginFormItemBinding
import pl.szczodrzynski.navlib.colorAttr
import java.util.*
import kotlin.coroutines.CoroutineContext
@ -31,6 +32,8 @@ import kotlin.coroutines.CoroutineContext
class LoginFormFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "LoginFormFragment"
// eggs
var wantEggs = false
}
private lateinit var app: App
@ -75,33 +78,53 @@ class LoginFormFragment : Fragment(), CoroutineScope {
b.subTitle.text = platformName ?: app.getString(mode.name)
b.text.text = platformGuideText ?: app.getString(mode.guideText)
val credentials = mutableMapOf<LoginInfo.Credential, LoginFormItemBinding>()
val credentials = mutableMapOf<LoginInfo.BaseCredential, Any>()
for (credential in mode.credentials) {
if (platformFormFields?.contains(credential.keyName) == false)
continue
val b = LoginFormItemBinding.inflate(layoutInflater)
b.textLayout.hint = app.getString(credential.name)
if (credential.hideText) {
b.textEdit.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD
b.textLayout.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
if (credential is LoginInfo.FormField) {
val b = LoginFormFieldItemBinding.inflate(layoutInflater)
b.textLayout.hint = app.getString(credential.name)
if (credential.hideText) {
b.textEdit.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD
b.textLayout.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
}
b.textEdit.addTextChangedListener {
b.textLayout.error = null
}
b.textEdit.id = credential.name
b.textEdit.setText(arguments?.getString(credential.keyName) ?: "")
b.textLayout.startIconDrawable = IconicsDrawable(activity)
.icon(credential.icon)
.sizeDp(24)
.paddingDp(2)
.colorAttr(activity, R.attr.colorOnBackground)
this.b.formContainer.addView(b.root)
credentials[credential] = b
}
b.textEdit.addTextChangedListener {
b.textLayout.error = null
if (credential is LoginInfo.FormCheckbox) {
val b = LoginFormCheckboxItemBinding.inflate(layoutInflater)
b.checkbox.text = app.getString(credential.name)
b.checkbox.onChange { _, isChecked ->
b.errorText.text = null
// eggs
if (register.internalName == "podlasie") {
wantEggs = !isChecked
}
}
if (arguments?.containsKey(credential.keyName) == true) {
b.checkbox.isChecked = arguments?.getBoolean(credential.keyName) == true
}
this.b.formContainer.addView(b.root)
credentials[credential] = b
}
b.textEdit.id = credential.name
b.textEdit.setText(arguments?.getString(credential.keyName) ?: "")
b.textLayout.startIconDrawable = IconicsDrawable(activity)
.icon(credential.icon)
.sizeDp(24)
.paddingDp(2)
.colorAttr(activity, R.attr.colorOnBackground)
this.b.formContainer.addView(b.root)
credentials[credential] = b
}
activity.lastError?.let { error ->
@ -109,7 +132,12 @@ class LoginFormFragment : Fragment(), CoroutineScope {
startCoroutineTimer(delayMillis = 200L) {
for (credential in credentials) {
credential.key.errorCodes[error.errorCode]?.let {
credential.value.textLayout.error = app.getString(it)
(credential.value as? LoginFormFieldItemBinding)?.let { b ->
b.textLayout.error = app.getString(it)
}
(credential.value as? LoginFormCheckboxItemBinding)?.let { b ->
b.errorText.text = app.getString(it)
}
return@startCoroutineTimer
}
}
@ -127,7 +155,7 @@ class LoginFormFragment : Fragment(), CoroutineScope {
"loginMode" to loginMode
)
if (App.devMode && b.fakeLogin.isChecked) {
if (App.debugMode && b.fakeLogin.isChecked) {
payload.putBoolean("fakeLogin", true)
}
@ -137,35 +165,42 @@ class LoginFormFragment : Fragment(), CoroutineScope {
var hasErrors = false
credentials.forEach { (credential, b) ->
var text = b.textEdit.text?.toString() ?: return@forEach
if (!credential.hideText)
text = text.trim()
if (credential is LoginInfo.FormField && b is LoginFormFieldItemBinding) {
var text = b.textEdit.text?.toString() ?: return@forEach
if (!credential.hideText)
text = text.trim()
if (credential.caseMode == LoginInfo.Credential.CaseMode.UPPER_CASE)
text = text.toUpperCase(Locale.getDefault())
if (credential.caseMode == LoginInfo.Credential.CaseMode.LOWER_CASE)
text = text.toLowerCase(Locale.getDefault())
if (credential.caseMode == LoginInfo.FormField.CaseMode.UPPER_CASE)
text = text.toUpperCase(Locale.getDefault())
if (credential.caseMode == LoginInfo.FormField.CaseMode.LOWER_CASE)
text = text.toLowerCase(Locale.getDefault())
credential.stripTextRegex?.let {
text = text.replace(it.toRegex(), "")
credential.stripTextRegex?.let {
text = text.replace(it.toRegex(), "")
}
b.textEdit.setText(text)
if (credential.isRequired && text.isBlank()) {
b.textLayout.error = app.getString(credential.emptyText)
hasErrors = true
return@forEach
}
if (!text.matches(credential.validationRegex.toRegex())) {
b.textLayout.error = app.getString(credential.invalidText)
hasErrors = true
return@forEach
}
payload.putString(credential.keyName, text)
arguments?.putString(credential.keyName, text)
}
b.textEdit.setText(text)
if (credential.isRequired && text.isBlank()) {
b.textLayout.error = app.getString(credential.emptyText)
hasErrors = true
return@forEach
if (credential is LoginInfo.FormCheckbox && b is LoginFormCheckboxItemBinding) {
val checked = b.checkbox.isChecked
payload.putBoolean(credential.keyName, checked)
arguments?.putBoolean(credential.keyName, checked)
}
if (!text.matches(credential.validationRegex.toRegex())) {
b.textLayout.error = app.getString(credential.invalidText)
hasErrors = true
return@forEach
}
payload.putString(credential.keyName, text)
arguments?.putString(credential.keyName, text)
}
if (hasErrors)

View File

@ -15,7 +15,7 @@ import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel
object LoginInfo {
private fun getEmailCredential(keyName: String) = Credential(
private fun getEmailCredential(keyName: String) = FormField(
keyName = keyName,
name = R.string.login_hint_email,
icon = CommunityMaterial.Icon.cmd_at,
@ -24,9 +24,9 @@ object LoginInfo {
errorCodes = mapOf(),
isRequired = true,
validationRegex = "([\\w.\\-_+]+)?\\w+@[\\w-_]+(\\.\\w+)+",
caseMode = Credential.CaseMode.LOWER_CASE
caseMode = FormField.CaseMode.LOWER_CASE
)
private fun getPasswordCredential(keyName: String) = Credential(
private fun getPasswordCredential(keyName: String) = FormField(
keyName = keyName,
name = R.string.login_hint_password,
icon = CommunityMaterial.Icon2.cmd_lock_outline,
@ -94,7 +94,7 @@ object LoginInfo {
hintText = R.string.login_mode_librus_jst_hint,
guideText = R.string.login_mode_librus_jst_guide,
credentials = listOf(
Credential(
FormField(
keyName = "accountCode",
name = R.string.login_hint_token,
icon = CommunityMaterial.Icon.cmd_code_braces,
@ -103,9 +103,9 @@ object LoginInfo {
errorCodes = mapOf(),
isRequired = true,
validationRegex = "[A-Z0-9_]+",
caseMode = Credential.CaseMode.UPPER_CASE
caseMode = FormField.CaseMode.UPPER_CASE
),
Credential(
FormField(
keyName = "accountPin",
name = R.string.login_hint_pin,
icon = CommunityMaterial.Icon2.cmd_lock,
@ -114,7 +114,7 @@ object LoginInfo {
errorCodes = mapOf(),
isRequired = true,
validationRegex = "[a-z0-9_]+",
caseMode = Credential.CaseMode.LOWER_CASE
caseMode = FormField.CaseMode.LOWER_CASE
)
),
errorCodes = mapOf(
@ -131,14 +131,14 @@ object LoginInfo {
registerLogo = R.drawable.login_logo_vulcan,
loginModes = listOf(
Mode(
loginMode = LOGIN_MODE_VULCAN_API,
loginMode = LOGIN_MODE_VULCAN_HEBE,
name = R.string.login_mode_vulcan_api,
icon = R.drawable.login_mode_vulcan_api,
icon = R.drawable.login_mode_vulcan_hebe,
hintText = R.string.login_mode_vulcan_api_hint,
guideText = R.string.login_mode_vulcan_api_guide,
isRecommended = true,
credentials = listOf(
Credential(
FormField(
keyName = "deviceToken",
name = R.string.login_hint_token,
icon = CommunityMaterial.Icon.cmd_code_braces,
@ -149,9 +149,9 @@ object LoginInfo {
),
isRequired = true,
validationRegex = "[A-Z0-9]{5,12}",
caseMode = Credential.CaseMode.UPPER_CASE
caseMode = FormField.CaseMode.UPPER_CASE
),
Credential(
FormField(
keyName = "symbol",
name = R.string.login_hint_symbol,
icon = CommunityMaterial.Icon2.cmd_school,
@ -162,9 +162,9 @@ object LoginInfo {
),
isRequired = true,
validationRegex = "[a-z0-9_-]+",
caseMode = Credential.CaseMode.LOWER_CASE
caseMode = FormField.CaseMode.LOWER_CASE
),
Credential(
FormField(
keyName = "devicePin",
name = R.string.login_hint_pin,
icon = CommunityMaterial.Icon2.cmd_lock,
@ -175,24 +175,24 @@ object LoginInfo {
),
isRequired = true,
validationRegex = "[0-9]+",
caseMode = Credential.CaseMode.LOWER_CASE
caseMode = FormField.CaseMode.LOWER_CASE
)
),
errorCodes = mapOf(
ERROR_LOGIN_VULCAN_EXPIRED_TOKEN to R.string.login_error_expired_token
)
)/*,
),
Mode(
loginMode = LOGIN_MODE_VULCAN_WEB,
name = R.string.login_mode_vulcan_web,
icon = R.drawable.login_mode_vulcan_web,
hintText = R.string.login_mode_vulcan_web_hint,
guideText = R.string.login_mode_vulcan_web_guide,
isTesting = true,
isDevOnly = true,
isPlatformSelection = true,
credentials = listOf(
getEmailCredential("webEmail"),
Credential(
FormField(
keyName = "webUsername",
name = R.string.login_hint_username,
icon = CommunityMaterial.Icon.cmd_account_outline,
@ -201,12 +201,12 @@ object LoginInfo {
errorCodes = mapOf(),
isRequired = true,
validationRegex = "[A-Z]{7}[0-9]+",
caseMode = Credential.CaseMode.UPPER_CASE
caseMode = FormField.CaseMode.UPPER_CASE
),
getPasswordCredential("webPassword")
),
errorCodes = mapOf()
)*/
)
)
),
Register(
@ -222,7 +222,7 @@ object LoginInfo {
hintText = R.string.login_mode_mobidziennik_web_hint,
guideText = R.string.login_mode_mobidziennik_web_guide,
credentials = listOf(
Credential(
FormField(
keyName = "username",
name = R.string.login_hint_login_email,
icon = CommunityMaterial.Icon.cmd_account_outline,
@ -231,9 +231,9 @@ object LoginInfo {
errorCodes = mapOf(),
isRequired = true,
validationRegex = "^[a-z0-9_\\-@+.]+$",
caseMode = Credential.CaseMode.LOWER_CASE
caseMode = FormField.CaseMode.LOWER_CASE
),
Credential(
FormField(
keyName = "password",
name = R.string.login_hint_password,
icon = CommunityMaterial.Icon2.cmd_lock_outline,
@ -246,7 +246,7 @@ object LoginInfo {
validationRegex = ".*",
hideText = true
),
Credential(
FormField(
keyName = "serverName",
name = R.string.login_hint_address,
icon = CommunityMaterial.Icon2.cmd_web,
@ -257,7 +257,7 @@ object LoginInfo {
),
isRequired = true,
validationRegex = "^[a-z0-9_\\-]+\$",
caseMode = Credential.CaseMode.LOWER_CASE
caseMode = FormField.CaseMode.LOWER_CASE
)
),
errorCodes = mapOf(
@ -280,7 +280,7 @@ object LoginInfo {
hintText = R.string.login_mode_idziennik_web_hint,
guideText = R.string.login_mode_idziennik_web_guide,
credentials = listOf(
Credential(
FormField(
keyName = "schoolName",
name = R.string.login_hint_school_name,
icon = CommunityMaterial.Icon2.cmd_school,
@ -291,9 +291,9 @@ object LoginInfo {
),
isRequired = true,
validationRegex = "^[a-z0-9_\\-.]+$",
caseMode = Credential.CaseMode.LOWER_CASE
caseMode = FormField.CaseMode.LOWER_CASE
),
Credential(
FormField(
keyName = "username",
name = R.string.login_hint_username,
icon = CommunityMaterial.Icon.cmd_account_outline,
@ -302,7 +302,7 @@ object LoginInfo {
errorCodes = mapOf(),
isRequired = true,
validationRegex = "^[a-z0-9_\\-.]+$",
caseMode = Credential.CaseMode.LOWER_CASE
caseMode = FormField.CaseMode.LOWER_CASE
),
getPasswordCredential("password")
),
@ -346,7 +346,7 @@ object LoginInfo {
icon = R.drawable.login_mode_podlasie_api,
guideText = R.string.login_mode_podlasie_api_guide,
credentials = listOf(
Credential(
FormField(
keyName = "apiToken",
name = R.string.login_hint_token,
icon = CommunityMaterial.Icon2.cmd_lock_outline,
@ -355,7 +355,15 @@ object LoginInfo {
errorCodes = mapOf(),
isRequired = true,
validationRegex = "[a-zA-Z0-9]{10}",
caseMode = Credential.CaseMode.UNCHANGED
caseMode = FormField.CaseMode.UNCHANGED
),
FormCheckbox(
keyName = "logoutDevices",
name = R.string.login_podlasie_logout_devices,
checked = false,
errorCodes = mapOf(
ERROR_LOGIN_PODLASIE_API_DEVICE_LIMIT to R.string.error_602_reason
)
)
),
errorCodes = mapOf()
@ -390,9 +398,10 @@ object LoginInfo {
val isRecommended: Boolean = false,
val isTesting: Boolean = false,
val isDevOnly: Boolean = false,
val isPlatformSelection: Boolean = false,
val credentials: List<Credential>,
val credentials: List<BaseCredential>,
val errorCodes: Map<Int, Int>
)
@ -409,11 +418,18 @@ object LoginInfo {
val apiData: JsonObject
)
data class Credential(
val keyName: String,
open class BaseCredential(
open val keyName: String,
@StringRes
open val name: Int,
open val errorCodes: Map<Int, Int>
)
data class FormField(
override val keyName: String,
@StringRes
val name: Int,
override val name: Int,
val icon: IIcon,
@StringRes
val placeholder: Int? = null,
@ -421,7 +437,7 @@ object LoginInfo {
val emptyText: Int,
@StringRes
val invalidText: Int,
val errorCodes: Map<Int, Int>,
override val errorCodes: Map<Int, Int>,
@StringRes
val hintText: Int? = null,
@ -430,10 +446,18 @@ object LoginInfo {
val caseMode: CaseMode = CaseMode.UNCHANGED,
val hideText: Boolean = false,
val stripTextRegex: String? = null
) {
) : BaseCredential(keyName, name, errorCodes) {
enum class CaseMode { UNCHANGED, UPPER_CASE, LOWER_CASE }
}
data class FormCheckbox(
override val keyName: String,
@StringRes
override val name: Int,
val checked: Boolean = false,
override val errorCodes: Map<Int, Int> = mapOf()
) : BaseCredential(keyName, name, errorCodes)
var chooserList: MutableList<Any>? = null
var platformList: MutableMap<Int, List<Platform>> = mutableMapOf()
}

View File

@ -47,6 +47,7 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope {
}
private lateinit var timeoutJob: Job
private lateinit var adapter: LoginPlatformAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (!isAdded) return
@ -57,12 +58,7 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope {
val loginMode = arguments?.getInt("loginMode") ?: return
val mode = register.loginModes.firstOrNull { it.loginMode == loginMode } ?: return
timeoutJob = startCoroutineTimer(5000L) {
b.timeoutText.isVisible = true
timeoutJob.cancel()
}
val adapter = LoginPlatformAdapter(activity) { platform ->
adapter = LoginPlatformAdapter(activity) { platform ->
nav.navigate(R.id.loginFormFragment, Bundle(
"loginType" to platform.loginType,
"loginMode" to platform.loginMode,
@ -73,7 +69,30 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope {
), activity.navOptions)
}
loadPlatforms(register, mode)
b.reloadButton.isVisible = App.devMode
b.reloadButton.onClick {
LoginInfo.platformList.remove(mode.name)
loadPlatforms(register, mode)
}
b.list.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
addItemDecoration(SimpleDividerItemDecoration(context))
}
}
private fun loadPlatforms(register: LoginInfo.Register, mode: LoginInfo.Mode) {
launch {
timeoutJob = startCoroutineTimer(5000L) {
b.timeoutText.isVisible = true
timeoutJob.cancel()
}
b.loadingLayout.isVisible = true
b.list.isVisible = false
b.reloadButton.isEnabled = false
val platforms = LoginInfo.platformList[mode.name]
?: run {
api.runCatching(activity) {
@ -87,14 +106,11 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope {
adapter.items = platforms
b.list.adapter = adapter
b.list.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
addItemDecoration(SimpleDividerItemDecoration(context))
}
timeoutJob.cancel()
b.loadingLayout.isVisible = false
b.list.isVisible = true
b.reloadButton.isEnabled = true
}
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-10-18.
*/
package pl.szczodrzynski.edziennik.ui.modules.login
import android.os.Bundle
import android.os.Process
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import coil.api.load
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.LoginPrizeFragmentBinding
import pl.szczodrzynski.edziennik.onClick
import kotlin.coroutines.CoroutineContext
import kotlin.system.exitProcess
class LoginPrizeFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "LoginPrizeFragment"
}
private lateinit var app: App
private lateinit var activity: LoginActivity
private lateinit var b: LoginPrizeFragmentBinding
private val nav by lazy { activity.nav }
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local/private variables go here
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as LoginActivity?) ?: return null
context ?: return null
app = activity.application as App
b = LoginPrizeFragmentBinding.inflate(inflater)
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
b.button.load("https://szkolny.eu/game/button.png")
b.button.onClick {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.are_you_sure)
.setMessage(R.string.dev_mode_enable_warning)
.setPositiveButton(R.string.yes) { _, _ ->
app.config.debugMode = true
App.devMode = true
MaterialAlertDialogBuilder(activity)
.setTitle("Restart")
.setMessage("Wymagany restart aplikacji")
.setPositiveButton(R.string.ok) { _, _ ->
Process.killProcess(Process.myPid())
Runtime.getRuntime().exit(0)
exitProcess(0)
}
.setCancelable(false)
.show()
}
.setNegativeButton(R.string.no) { _, _ ->
app.config.debugMode = false
App.devMode = false
activity.finish()
}
.show()
}
}
}

View File

@ -10,7 +10,10 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.LoginChooserModeItemBinding
import pl.szczodrzynski.edziennik.resolveColor
import pl.szczodrzynski.edziennik.setTintColor
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
import pl.szczodrzynski.edziennik.ui.modules.login.LoginChooserAdapter
import pl.szczodrzynski.edziennik.ui.modules.login.LoginInfo
@ -34,6 +37,19 @@ class ModeViewHolder(
b.description.isVisible = true
b.description.setText(item.hintText)
}
b.hint.isVisible = false
b.badge.isVisible = item.isRecommended || item.isDevOnly || item.isTesting
if (item.isRecommended) {
b.badge.setText(R.string.login_chooser_mode_recommended)
b.badge.background.setTintColor(R.color.md_blue_300.resolveColor(app))
}
if (item.isTesting) {
b.badge.setText(R.string.login_chooser_mode_testing)
b.badge.background.setTintColor(R.color.md_yellow_300.resolveColor(app))
}
if (item.isDevOnly) {
b.badge.setText(R.string.login_chooser_mode_dev_only)
b.badge.background.setTintColor(R.color.md_red_300.resolveColor(app))
}
}
}

View File

@ -108,7 +108,7 @@ class MessageFragment : Fragment(), CoroutineScope {
.setNegativeButton(R.string.cancel, null)
.show()
}
b.downloadButton.isVisible = App.debugMode
b.downloadButton.isVisible = App.devMode
b.downloadButton.onClick {
EdziennikTask.messageGet(App.profileId, message).enqueue(activity)
}

View File

@ -581,7 +581,7 @@ public class SettingsNewFragment extends MaterialAboutFragment {
syncCardIntervalItem.setChecked(app.getConfig().getSync().getEnabled());
syncCardIntervalItem.setOnClickAction(() -> {
List<CharSequence> intervalNames = new ArrayList<>();
if (App.Companion.getDevMode() && false) {
if (App.Companion.getDebugMode() && false) {
intervalNames.add(ExtensionsKt.plural(activity, R.plurals.time_till_seconds, 30));
intervalNames.add(ExtensionsKt.plural(activity, R.plurals.time_till_minutes, 2));
}
@ -593,7 +593,7 @@ public class SettingsNewFragment extends MaterialAboutFragment {
intervalNames.add(ExtensionsKt.plural(activity, R.plurals.time_till_hours, 3));
intervalNames.add(ExtensionsKt.plural(activity, R.plurals.time_till_hours, 4));
List<Integer> intervals = new ArrayList<>();
if (App.Companion.getDevMode() && false) {
if (App.Companion.getDebugMode() && false) {
intervals.add(30);
intervals.add(2 * 60);
}
@ -1059,6 +1059,24 @@ public class SettingsNewFragment extends MaterialAboutFragment {
);
}
if (App.Companion.getDevMode()) {
items.add(
new MaterialAboutSwitchItem(
getString(R.string.settings_register_hide_sticks_from_old),
null,
new IconicsDrawable(activity)
.icon(CommunityMaterial.Icon2.cmd_numeric_1_box_outline)
.size(IconicsSize.dp(iconSizeDp))
.color(IconicsColor.colorInt(iconColor))
)
.setChecked(app.getConfig().forProfile().getGrades().getHideSticksFromOld())
.setOnChangeAction((isChecked, tag) -> {
app.getConfig().forProfile().getGrades().setHideSticksFromOld(isChecked);
return true;
})
);
}
}
return items;
}
@ -1245,7 +1263,7 @@ public class SettingsNewFragment extends MaterialAboutFragment {
})
.build());*/
if (App.Companion.getDebugMode()) {
if (App.Companion.getDevMode()) {
items.add(new MaterialAboutActionItem.Builder()
.text(R.string.settings_about_crash_text)
.subText(R.string.settings_about_crash_subtext)

View File

@ -108,7 +108,7 @@ public class Utils {
public static List<String> debugLog = new ArrayList<>();
public static void d(String TAG, String message) {
if (App.Companion.getDebugMode()) {
if (App.Companion.getDevMode()) {
HyperLog.d("Szkolny/"+TAG, message);
//debugLog.add(TAG+": "+message);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -8,7 +8,7 @@
<data>
<import type="android.view.View" />
<import type="android.text.Html" />
<import type="androidx.core.text.HtmlCompat" />
<variable
name="message"
type="pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus.Message" />
@ -45,7 +45,7 @@
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{message.title}"
android:text="@{HtmlCompat.fromHtml(message.title, HtmlCompat.FROM_HTML_MODE_LEGACY)}"
android:textAppearance="@style/NavView.TextView.Title"
tools:text="Dziennik nie działa" />
@ -54,7 +54,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@{Html.fromHtml(message.contentLong)}"
android:text="@{HtmlCompat.fromHtml(message.contentLong, HtmlCompat.FROM_HTML_MODE_LEGACY)}"
tools:text="Dziennik się zepsuł i nie działa, szkoda\n\n\nwiele linijek ma ten tekst" />
<com.google.android.material.button.MaterialButton

View File

@ -7,6 +7,10 @@
xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="HardcodedText">
<data>
<variable name="app" type="pl.szczodrzynski.edziennik.App"/>
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -74,6 +78,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" />
<com.google.android.material.checkbox.MaterialCheckBox
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@={app.config.archiverEnabled}"
android:text="Archiver enabled" />
</LinearLayout>
</ScrollView>
</layout>

View File

@ -31,6 +31,7 @@
android:textSize="24sp" />
<TextView
android:id="@+id/subtitleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
@ -47,13 +48,14 @@
tools:listitem="@layout/login_chooser_item" />
<TextView
android:id="@+id/footnoteText"
style="@style/TextAppearance.AppCompat.Small"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
android:text="@string/login_copyright_notice"
android:textAlignment="center" />
android:gravity="center" />
<LinearLayout
android:layout_width="match_parent"

View File

@ -36,6 +36,19 @@
android:orientation="vertical"
android:gravity="center_vertical">
<com.mikepenz.iconics.view.IconicsTextView
android:id="@+id/badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_rounded_8dp"
android:minHeight="0dp"
android:paddingHorizontal="4dp"
android:paddingVertical="2dp"
android:textColor="@color/md_black_1000"
android:textSize="12sp"
tools:backgroundTint="@color/md_blue_300"
tools:text="{cmd-alert-circle-outline} Zalecane" />
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
@ -43,13 +56,6 @@
android:textAppearance="@style/NavView.TextView.Medium"
tools:text="Zaloguj używając e-maila" />
<TextView
android:id="@+id/hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
tools:text="(zalecane)" />
<TextView
android:id="@+id/description"
android:layout_width="wrap_content"

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-10-16.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="4dp">
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="0dp"
tools:text="Text" />
<TextView
android:id="@+id/errorText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="32dp"
android:textColor="?colorError"
tools:text="Error text" />
</LinearLayout>
</layout>

View File

@ -78,6 +78,15 @@
tools:visibility="visible" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/reloadButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/refresh"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-10-18.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<ImageView
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="48dp"
android:adjustViewBounds="true"
android:scaleType="fitXY"
tools:src="@tools:sample/avatars" />
</FrameLayout>
</layout>

View File

@ -18,6 +18,24 @@
<action
android:id="@+id/action_loginChooserFragment_to_loginFormFragment"
app:destination="@id/loginFormFragment" />
<!-- eggs -->
<action
android:id="@+id/action_loginChooserFragment_to_loginEggsFragment"
app:destination="@id/loginEggsFragment" />
</fragment>
<!-- eggs -->
<fragment
android:id="@+id/loginEggsFragment"
android:name="pl.szczodrzynski.edziennik.ui.modules.login.LoginEggsFragment"
android:label="LoginEggsFragment">
<action
android:id="@+id/action_loginEggsFragment_to_loginPrizeFragment"
app:destination="@id/loginPrizeFragment" />
</fragment>
<fragment
android:id="@+id/loginPrizeFragment"
android:name="pl.szczodrzynski.edziennik.ui.modules.login.LoginPrizeFragment"
android:label="LoginPrizeFragment">
</fragment>
<fragment
android:id="@+id/loginPlatformListFragment"

View File

@ -854,7 +854,7 @@
<string name="settings_about_licenses_text">Open-Source-Lizenzen</string>
<string name="settings_about_privacy_policy_text">Datenschutzrichtlinie</string>
<string name="settings_about_register_title_text">E-Klassenbuch</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - August 2020</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - Februar 2021</string>
<string name="settings_about_update_subtext">Klicken Sie hier, um nach Aktualisierungen zu suchen</string>
<string name="settings_about_update_text">Aktualisierung</string>
<string name="settings_about_version_text">Version</string>

View File

@ -856,7 +856,7 @@
<string name="settings_about_licenses_text">Open-source licenses</string>
<string name="settings_about_privacy_policy_text">Privacy policy</string>
<string name="settings_about_register_title_text">E-register</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - August 2020</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - February 2021</string>
<string name="settings_about_update_subtext">Click to check for updates</string>
<string name="settings_about_update_text">Update</string>
<string name="settings_about_version_text">Version</string>
@ -1233,4 +1233,5 @@
<string name="permissions_attachment">In order to download the file, you have to grant file storage permission for the application.\n\nClick OK to grant the permission.</string>
<string name="permissions_denied">You denied the required permissions for the application.\n\nIn order to grant the permission, open the Permissions screen for Szkolny.eu in phone settings.\n\nClick OK to open app settings now.</string>
<string name="permissions_required">Required permissions</string>
<string name="settings_register_hide_sticks_from_old">Your mother won\'t see your F grades</string>
</resources>

View File

@ -130,6 +130,7 @@
<string name="error_341" translatable="false">ERROR_VULCAN_API_BAD_REQUEST</string>
<string name="error_342" translatable="false">ERROR_VULCAN_API_OTHER</string>
<string name="error_343" translatable="false">ERROR_VULCAN_ATTACHMENT_DOWNLOAD</string>
<string name="error_390" translatable="false">ERROR_VULCAN_API_DEPRECATED</string>
<string name="error_401" translatable="false">ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN</string>
<string name="error_402" translatable="false">ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME</string>
@ -318,6 +319,7 @@
<string name="error_341_reason">VULCAN®: błąd żądania, zgłoś błąd</string>
<string name="error_342_reason">VULCAN®: inny błąd, wyślij zgłoszenie</string>
<string name="error_343_reason">VULCAN®: nie znaleziono adresu załącznika</string>
<string name="error_390_reason">W związku z wygaszeniem aplikacji Dzienniczek+ przez firmę Vulcan, należy zalogować się ponownie.\n\nAby móc dalej korzystać z aplikacji Szkolny.eu, otwórz Ustawienia i wybierz opcję Dodaj nowego ucznia.\nNastępnie zaloguj się do dziennika Vulcan zgodnie z instrukcją.\n\nPrzepraszamy za niedogodności.</string>
<string name="error_401_reason">Nieprawidłowe dane logowania</string>
<string name="error_402_reason">Nieprawidłowa nazwa szkoły</string>

View File

@ -919,7 +919,7 @@
<string name="settings_about_licenses_text">Licencje open-source</string>
<string name="settings_about_privacy_policy_text">Polityka prywatności</string>
<string name="settings_about_register_title_text">E-dziennik</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nwrzesień 2018 - sierpień 2020</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nwrzesień 2018 - luty 2021</string>
<string name="settings_about_update_subtext">Kliknij, aby sprawdzić aktualizacje</string>
<string name="settings_about_update_text">Aktualizacja</string>
<string name="settings_about_version_text">Wersja</string>
@ -1368,7 +1368,7 @@
<string name="home_archive_close_no_target_title">Brak aktualnego profilu</string>
<string name="home_archive_close_no_target_text">Uczeń %s nie posiada profilu na tym koncie w aktualnym roku szkolnym. Prawdopodobnie ten profil został usunięty lub uczeń nie uczęszcza już do tej klasy.\n\nAby przejść do aktualnego profilu, wybierz ucznia z listy lub zaloguj się na jego konto przyciskiem Dodaj ucznia.</string>
<string name="login_copyright_notice">Znaki towarowe zamieszczone w tej aplikacji należą do ich prawowitych właścicieli i są używane wyłącznie w celach informacyjnych.</string>
<string name="update_available_title">Dostępna aktualiacja aplikacji</string>
<string name="update_available_title">Dostępna aktualizacja aplikacji</string>
<string name="update_available_format">Używasz starej wersji aplikacji Szkolny.eu (%s). Aby móc korzystać z aplikacji oraz zapewnić najlepsze działanie, zaktualizuj aplikację do wersji %s.\n\nDziennik zmian:\n%s</string>
<string name="update_available_fallback">Posiadasz nieaktualną wersję aplikacji Szkolny.eu. Aby móc dalej synchronizować dane, musisz zaktualizować aplikację.</string>
<string name="update_available_button">Aktualizuj</string>
@ -1378,4 +1378,12 @@
<string name="home_availability_info">Zobacz więcej</string>
<string name="home_availability_update">Aktualizuj</string>
<string name="register_unavailable_read_more">Dowiedz się więcej</string>
<string name="settings_register_hide_sticks_from_old">Stara nie zobaczy pał</string>
<string name="login_podlasie_logout_devices">Wyloguj z pozostałych urządzeń</string>
<string name="login_chooser_testing_title">Wersja testowa</string>
<string name="login_chooser_testing_text">Wybrany sposób logowania jest jeszcze w fazie testów i może nie działać poprawnie.\n\nJeśli masz problemy z aplikacją, wybierz zalecany sposób logowania.</string>
<string name="login_chooser_mode_recommended">{cmd-information-outline} Zalecane</string>
<string name="login_chooser_mode_testing">{cmd-alert-circle-outline} Wersja testowa</string>
<string name="login_chooser_mode_dev_only">{cmd-android-studio} Wersja deweloperska</string>
<string name="eggs">\???</string>
</resources>

View File

@ -2,43 +2,43 @@
buildscript {
ext {
kotlin_version = '1.3.61'
kotlin_version = '1.4.30'
release = [
versionName: "4.4",
versionCode: 4040099
versionName: "4.5.1-beta.1",
versionCode: 4050101
]
setup = [
compileSdk: 28,
compileSdk: 30,
buildTools: "28.0.3",
minSdk : 16,
targetSdk : 28
targetSdk : 30
]
versions = [
gradleAndroid : '4.1.0-rc01',
gradleAndroid : '4.2.0-beta04',
kotlin : ext.kotlin_version,
ktx : "1.2.0",
ktx : "1.3.2",
androidX : '1.0.0',
annotation : '1.1.0',
recyclerView : '1.2.0-alpha01',
material : '1.2.0-alpha05',
appcompat : '1.2.0-alpha02',
constraintLayout : '2.0.0-beta4',
recyclerView : '1.2.0-beta01',
material : '1.3.0',
appcompat : '1.3.0-beta01',
constraintLayout : '2.1.0-alpha2',
cardview : '1.0.0',
gridLayout : '1.0.0',
navigation : "2.0.0",
navigationFragment: "1.0.0",
legacy : "1.0.0",
room : "2.2.5",
lifecycle : "2.2.0",
work : "2.3.4",
room : "2.2.6",
lifecycle : "2.3.0",
work : "2.5.0",
firebase : '17.2.2',
firebase : '18.0.2',
firebasemessaging: "20.1.3",
play_services : "17.0.0",
@ -53,12 +53,11 @@ buildscript {
retrofit : "2.6.4"
]
versions.kotlin = '1.4.0'
versions.kotlin = '1.4.0'
}
repositories {
maven {
url 'https://maven.fabric.io/public'
}
google()
jcenter()
}
@ -66,11 +65,8 @@ buildscript {
classpath "com.android.tools.build:gradle:${versions.gradleAndroid}"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath 'me.tatarka:gradle-retrolambda:3.7.0'
classpath 'com.google.gms:google-services:4.3.3'
classpath 'io.fabric.tools:gradle:1.28.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath 'com.google.gms:google-services:4.3.5'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.0'
}
}

View File

@ -9,7 +9,6 @@
android.enableJetifier=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m
android.enableR8=true
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects

View File

@ -1,6 +1,6 @@
#Mon Aug 24 17:15:24 CEST 2020
#Wed Feb 17 14:04:38 CET 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
zipStoreBase=GRADLE_USER_HOME

View File

@ -1,3 +1,5 @@
include ':wear'
include ':wear'
include ':codegen'
include ':annotation'
rootProject.name='Szkolny.eu'

1
wear/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -1,37 +1,53 @@
apply plugin: 'com.android.application'
/*
* Copyright (c) Kacper Ziubryniewicz 2020-9-17
*/
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdkVersion 28
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "pl.szczodrzynski.edziennik"
minSdkVersion 23
targetSdkVersion 28
targetSdkVersion 29
versionCode 1
versionName "1.0"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'com.google.android.support:wearable:2.4.0'
implementation 'com.google.android.gms:play-services-wearable:16.0.1'
implementation 'androidx.percentlayout:percentlayout:1.0.0-beta01'
implementation 'androidx.legacy:legacy-support-v4:1.0.0-beta01'
implementation 'androidx.recyclerview:recyclerview:1.0.0-beta01'
implementation 'androidx.wear:wear:1.0.0-beta01'
compileOnly 'com.google.android.wearable:wearable:2.4.0'
implementation 'androidx.wear:wear:1.0.0'
implementation 'com.google.android.support:wearable:2.7.0'
compileOnly 'com.google.android.wearable:wearable:2.7.0'
implementation "com.google.android.gms:play-services-wearable:${versions.play_services}"
}

21
wear/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

Some files were not shown because too many files have changed in this diff Show More