Compare commits

...

32 Commits

Author SHA1 Message Date
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
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
110 changed files with 2875 additions and 641 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"
@ -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.** { *; }
-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,8 @@
<h3>Wersja 4.4, 2020-08-27</h3>
<h3>Wersja 4.5-beta.1, 2021-02-21</h3>
<ul>
<li>Poprawione komunikaty o aktualizacjach aplikacji.</li>
<li>Vulcan: aplikacja Szkolny.eu zaktualizowana w związku z wygaszeniem aplikacji Dzienniczek+.</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 };
0x35, 0x4c, 0x9d, 0x0e, 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

@ -299,7 +299,7 @@ fun colorFromCssName(name: String): Int {
"orange" -> 0xffffa500
"black" -> 0xff000000
"white" -> 0xffffffff
else -> -1
else -> -1L
}.toInt()
}
@ -1172,7 +1172,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

@ -232,7 +232,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 +566,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 +647,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) {

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,17 @@ 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_TIMETABLE = "api/mobile/schedule"
const val VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGES = "api/mobile/schedule/changes"
const val VULCAN_HEBE_ENDPOINT_EXAMS = "api/mobile/exam"
const val VULCAN_HEBE_ENDPOINT_GRADES = "api/mobile/grade"
const val VULCAN_HEBE_ENDPOINT_HOMEWORK = "api/mobile/homework"
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,7 @@ 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_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN = 401
const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME = 402
@ -229,5 +230,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,19 @@ 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_HEBE
}
.withRequiredLoginMethod { _, loginStore ->
if (loginStore.mode == LOGIN_MODE_VULCAN_WEB) LOGIN_METHOD_VULCAN_WEB_MAIN else 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
}
)

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

@ -20,25 +20,42 @@ 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_TIMETABLE = 3020
const val ENDPOINT_VULCAN_HEBE_EXAMS = 3030
const val ENDPOINT_VULCAN_HEBE_GRADES = 3040
const val ENDPOINT_VULCAN_HEBE_HOMEWORK = 3060
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
), 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

View File

@ -7,6 +7,10 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data
import pl.szczodrzynski.edziennik.R
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.VulcanHebeExams
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.VulcanHebeGrades
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.VulcanHebeHomework
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.VulcanHebeTimetable
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.web.VulcanWebLuckyNumber
import pl.szczodrzynski.edziennik.utils.Utils
@ -91,6 +95,22 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_lucky_number)
VulcanWebLuckyNumber(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_HOMEWORK -> {
data.startProgress(R.string.edziennik_progress_endpoint_homework)
VulcanHebeHomework(data, lastSync, onSuccess)
}
else -> onSuccess(endpointId)
}
}

View File

@ -0,0 +1,359 @@
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): Long {
val date = json.getJsonObject(key)
return date.getLong("Timestamp") ?: return System.currentTimeMillis()
}
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
}
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 = when (T::class.java) {
JsonObject::class.java -> json.getJsonObject("Envelope")
JsonArray::class.java -> json.getJsonArray("Envelope")
else -> {
data.error(ApiError(tag, ERROR_RESPONSE_EMPTY)
.withResponse(response)
.withApiResponse(json)
)
return
}
}
try {
onSuccess(envelope as T, 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()
query["lastId"] = "-2147483648" // don't ask, it's just Vulcan
query["pageSize"] = "500"
query["lastSyncDate"] = LocalDateTime
.ofInstant(Instant.ofEpochMilli(lastSync ?: 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

@ -0,0 +1,7 @@
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,78 @@
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() ?: ""
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,121 @@
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,69 @@
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() ?: ""
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,160 @@
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 = unit.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,247 @@
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.getString("Room")
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.getString("Room") ?: 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,106 @@
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.MTIzNDU2Nzg5MDQLA8n9Ff===.$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

@ -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,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(
@ -138,7 +138,7 @@ object LoginInfo {
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,76 @@ 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_HEBE,
name = R.string.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,
isTesting = true,
credentials = listOf(
FormField(
keyName = "deviceToken",
name = R.string.login_hint_token,
icon = CommunityMaterial.Icon.cmd_code_braces,
emptyText = R.string.login_error_no_token,
invalidText = R.string.login_error_incorrect_token,
errorCodes = mapOf(
ERROR_LOGIN_VULCAN_INVALID_TOKEN to R.string.login_error_incorrect_token
),
isRequired = true,
validationRegex = "[A-Z0-9]{5,12}",
caseMode = FormField.CaseMode.UPPER_CASE
),
FormField(
keyName = "symbol",
name = R.string.login_hint_symbol,
icon = CommunityMaterial.Icon2.cmd_school,
emptyText = R.string.login_error_no_symbol,
invalidText = R.string.login_error_incorrect_symbol,
errorCodes = mapOf(
ERROR_LOGIN_VULCAN_INVALID_SYMBOL to R.string.login_error_incorrect_symbol
),
isRequired = true,
validationRegex = "[a-z0-9_-]+",
caseMode = FormField.CaseMode.LOWER_CASE
),
FormField(
keyName = "devicePin",
name = R.string.login_hint_pin,
icon = CommunityMaterial.Icon2.cmd_lock,
emptyText = R.string.login_error_no_pin,
invalidText = R.string.login_error_incorrect_pin,
errorCodes = mapOf(
ERROR_LOGIN_VULCAN_INVALID_PIN to R.string.login_error_incorrect_pin
),
isRequired = true,
validationRegex = "[0-9]+",
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 +253,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 +274,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 +283,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 +298,7 @@ object LoginInfo {
validationRegex = ".*",
hideText = true
),
Credential(
FormField(
keyName = "serverName",
name = R.string.login_hint_address,
icon = CommunityMaterial.Icon2.cmd_web,
@ -257,7 +309,7 @@ object LoginInfo {
),
isRequired = true,
validationRegex = "^[a-z0-9_\\-]+\$",
caseMode = Credential.CaseMode.LOWER_CASE
caseMode = FormField.CaseMode.LOWER_CASE
)
),
errorCodes = mapOf(
@ -280,7 +332,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 +343,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 +354,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 +398,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 +407,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 +450,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 +470,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 +489,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 +498,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

@ -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

@ -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-beta.1",
versionCode: 4050001
]
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

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-9-17
*/
package pl.szczodrzynski.edziennik
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("pl.szczodrzynski.edziennik", appContext.packageName)
}
}

View File

@ -1,44 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kacper Ziubryniewicz 2020-9-17
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="pl.szczodrzynski.edziennik">
<uses-feature android:name="android.hardware.type.watch" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Required to act as a custom watch face. -->
<uses-permission android:name="android.permission.WAKE_LOCK" /> <!-- Required for complications to receive complication data and open the provider chooser. -->
<uses-permission android:name="com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA" />
<uses-feature android:name="android.hardware.type.watch" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="false" />
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
android:theme="@style/Theme.Szkolnyeu">
<uses-library
android:name="com.google.android.wearable"
android:required="true" />
<!--
Set to true if your app is Standalone, that is, it does not require the handheld
app to run.
-->
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="true" />
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppThemeDark">
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="android.support.wearable.activity.ConfirmationActivity">
</activity>
</application>
</manifest>
</manifest>

View File

@ -1,153 +0,0 @@
package pl.szczodrzynski.edziennik;
import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import androidx.wear.widget.drawer.WearableDrawerLayout;
import androidx.wear.widget.drawer.WearableDrawerView;
import androidx.wear.widget.drawer.WearableNavigationDrawerView;
import android.support.wearable.activity.WearableActivity;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.wearable.CapabilityClient;
import com.google.android.gms.wearable.CapabilityInfo;
import com.google.android.gms.wearable.DataEvent;
import com.google.android.gms.wearable.DataItem;
import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.PutDataMapRequest;
import com.google.android.gms.wearable.PutDataRequest;
import com.google.android.gms.wearable.Wearable;
import java.util.Arrays;
import java.util.Set;
public class MainActivity extends WearableActivity {
private static final String TAG = "MainActivity";
private ProgressBar progressBar;
private WearableDrawerLayout wearableDrawerLayout;
private WearableNavigationDrawerView mWearableNavigationDrawer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Enables Always-on
setAmbientEnabled();
progressBar = findViewById(R.id.progressBar);
wearableDrawerLayout = findViewById(R.id.drawer_layout);
wearableDrawerLayout.setDrawerStateCallback(new WearableDrawerLayout.DrawerStateCallback() {
@Override
public void onDrawerOpened(WearableDrawerLayout layout, WearableDrawerView drawerView) {
super.onDrawerOpened(layout, drawerView);
}
@Override
public void onDrawerClosed(WearableDrawerLayout layout, WearableDrawerView drawerView) {
super.onDrawerClosed(layout, drawerView);
progressBar.setVisibility(View.GONE);
}
});
mWearableNavigationDrawer = (WearableNavigationDrawerView) findViewById(R.id.top_navigation_drawer);
WearableNavigationDrawerView.WearableNavigationDrawerAdapter navigationDrawerAdapter = new NavigationDrawerAdapter(this);
mWearableNavigationDrawer.setAdapter(navigationDrawerAdapter);
mWearableNavigationDrawer.addOnItemSelectedListener(new WearableNavigationDrawerView.OnItemSelectedListener() {
@Override
public void onItemSelected(int i) {
//Toast.makeText(MainActivity.this, "Selected item "+i, Toast.LENGTH_SHORT).show();
progressBar.setVisibility(View.VISIBLE);
}
});
// Peeks navigation drawer on the top.
mWearableNavigationDrawer.getController().peekDrawer();
Wearable.getMessageClient(this).addListener(messageEvent -> {
Log.d(TAG, messageEvent.getPath()+" :: "+ Arrays.toString(messageEvent.getData()));
});
Task<CapabilityInfo> capabilityInfoTask =
Wearable.getCapabilityClient(this)
.getCapability("edziennik_phone_app", CapabilityClient.FILTER_REACHABLE);
capabilityInfoTask.addOnCompleteListener((task) -> {
if (task.isSuccessful()) {
CapabilityInfo capabilityInfo = task.getResult();
assert capabilityInfo != null;
Set<Node> nodes;
nodes = capabilityInfo.getNodes();
Log.d(TAG, "Nodes "+nodes);
} else {
Log.d(TAG, "Capability request failed to return any results.");
}
});
Wearable.getDataClient(this).addListener(dataEventBuffer -> {
Log.d(TAG, "onDataChanged(): " + dataEventBuffer);
for (DataEvent event : dataEventBuffer) {
if (event.getType() == DataEvent.TYPE_CHANGED) {
String path = event.getDataItem().getUri().getPath();
Log.d(TAG, "Data "+path+ " :: "+Arrays.toString(event.getDataItem().getData()));
}
}
});
findViewById(R.id.test).setOnClickListener((v -> {
PutDataMapRequest putDataMapRequest = PutDataMapRequest.create("/ping");
putDataMapRequest.getDataMap().putLong("millis", System.currentTimeMillis());
PutDataRequest request = putDataMapRequest.asPutDataRequest();
request.setData("Hello".getBytes());
request.setUrgent();
Log.d(TAG, "Generating DataItem: " + request);
Task<DataItem> dataItemTask =
Wearable.getDataClient(getApplicationContext()).putDataItem(request);
dataItemTask.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Log.d(TAG, "success");
} else {
Log.d(TAG, "Capability request failed to return any results.");
}
});
}));
// Block on a task and get the result synchronously (because this is on a background
// thread).
//DataItem dataItem = dataItemTask.getResult();
//Log.d(TAG, "DataItem saved: " + dataItem);
}
private class NavigationDrawerAdapter extends WearableNavigationDrawerView.WearableNavigationDrawerAdapter {
public NavigationDrawerAdapter(Activity activity) {
}
@Override
public CharSequence getItemText(int i) {
return "Item "+i;
}
@Override
public Drawable getItemDrawable(int i) {
return null;
}
@Override
public int getCount() {
return 5;
}
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-9-17
*/
package pl.szczodrzynski.edziennik
import android.os.Bundle
import android.support.wearable.activity.WearableActivity
import com.google.android.gms.wearable.*
class MainActivity : WearableActivity(), DataClient.OnDataChangedListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Enables Always-on
setAmbientEnabled()
}
override fun onDataChanged(dataEvents: DataEventBuffer) {
dataEvents.forEach { event ->
if (event.type == DataEvent.TYPE_CHANGED) {
event.dataItem.also { item ->
if (item?.uri?.path?.compareTo("/test") == 0) {
DataMapItem.fromDataItem(item).dataMap.apply {
getInt("test")
}
}
}
} else if (event.type == DataEvent.TYPE_DELETED) {
// DataItem deleted
}
}
}
override fun onResume() {
super.onResume()
Wearable.getDataClient(this).addListener(this)
}
override fun onPause() {
super.onPause()
Wearable.getDataClient(this).removeListener(this)
}
}

View File

@ -0,0 +1,34 @@
<!--
~ Copyright (c) Kacper Ziubryniewicz 2020-9-17
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kacper Ziubryniewicz 2020-9-17
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -1,47 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.wear.widget.drawer.WearableDrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) Kacper Ziubryniewicz 2020-9-17
-->
<androidx.wear.widget.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="@color/white"
android:padding="@dimen/box_inset_layout_padding"
tools:context=".MainActivity"
tools:deviceIds="wear">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:padding="@dimen/inner_frame_layout_padding"
app:boxedEdges="all"
tools:ignore="MissingPrefix">
<ScrollView
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:nestedScrollingEnabled="true">
<LinearLayout
android:id="@+id/linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"/>
</LinearLayout>
</ScrollView>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
android:text="@string/hello_world" />
</FrameLayout>
<androidx.wear.widget.drawer.WearableNavigationDrawerView
android:id="@+id/top_navigation_drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
app:navigationStyle="multiPage"/>
</androidx.wear.widget.drawer.WearableDrawerLayout>
</androidx.wear.widget.BoxInsetLayout>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kacper Ziubryniewicz 2020-9-17
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kacper Ziubryniewicz 2020-9-17
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

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