diff --git a/.gitignore b/.gitignore index 047ad745..25786692 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ captures/ .idea/modules.xml # Comment next line if keeping position of elements in Navigation Editor is relevant for you .idea/navEditor.xml +.idea/copyright/profiles_settings.xml # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. @@ -80,3 +81,9 @@ lint/generated/ lint/outputs/ lint/tmp/ # lint/reports/ + +app/schemas/ + +signatures/ + +app/.cxx \ No newline at end of file diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 00000000..9ffcbb8a --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Szkolny.eu \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index 61f16974..00000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 00000000..6afdce41 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index f834a4ed..24f8c447 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,6 +5,13 @@ + + + + + + + - + diff --git a/agendacalendarview/build.gradle b/agendacalendarview/build.gradle index 28cbb26d..824ce517 100644 --- a/agendacalendarview/build.gradle +++ b/agendacalendarview/build.gradle @@ -49,6 +49,6 @@ dependencies { // other libraries //implementation 'se.emilsjolander:stickylistheaders:2.7.0' - implementation 'com.github.edisonw:StickyListHeaders:master-SNAPSHOT' + implementation 'com.github.edisonw:StickyListHeaders:master-SNAPSHOT@aar' implementation 'io.reactivex:rxjava:1.1.1' } diff --git a/app/build.gradle b/app/build.gradle index 3abf1031..f7131927 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,6 +15,12 @@ android { versionCode release.versionCode versionName release.versionName multiDexEnabled true + + externalNativeBuild { + cmake { + cppFlags "-std=c++11" + } + } } buildTypes { applicationVariants.all { variant -> @@ -62,6 +68,12 @@ android { packagingOptions { exclude 'META-INF/library-core_release.kotlin_module' } + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" + version "3.10.2" + } + } } /*task finalizeBundleDebug(type: Copy) { @@ -131,7 +143,7 @@ dependencies { implementation("com.github.ozodrukh:CircularReveal:2.0.1@aar") {transitive = true} implementation "com.heinrichreimersoftware:material-intro:1.5.8" // do not update implementation "com.jaredrummler:colorpicker:1.0.2" - implementation "com.squareup.okhttp3:okhttp:3.12.0" + implementation "com.squareup.okhttp3:okhttp:3.12.2" implementation "com.theartofdev.edmodo:android-image-cropper:2.8.0" // do not update implementation "com.wdullaer:materialdatetimepicker:4.1.2" implementation "com.yuyh.json:jsonviewer:1.0.6" @@ -142,7 +154,7 @@ dependencies { implementation "org.jsoup:jsoup:1.10.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' + implementation 'com.github.edisonw:StickyListHeaders:master-SNAPSHOT@aar' implementation "uk.co.samuelwall:material-tap-target-prompt:2.14.0" implementation project(":agendacalendarview") @@ -152,6 +164,29 @@ dependencies { implementation project(":nachos") //implementation project(":Navigation") implementation project(":szkolny-font") + + debugImplementation "com.github.ChuckerTeam.Chucker:library:3.0.1" + releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:3.0.1" + + //implementation 'com.github.wulkanowy:uonet-request-signer:master-SNAPSHOT' + //implementation 'com.github.kuba2k2.uonet-request-signer:android:master-63f094b14a-1' + + //implementation "org.redundent:kotlin-xml-builder:1.5.3" + + implementation "io.github.wulkanowy:signer-android:0.1.1" + + implementation "androidx.work:work-runtime-ktx:${versions.work}" + + implementation 'com.hypertrack:hyperlog:0.0.10' + + implementation 'com.github.kuba2k2:RecyclerTabLayout:700f980584' + + implementation 'com.github.kuba2k2:Tachyon:551943a6b5' + + implementation "com.squareup.retrofit2:retrofit:${versions.retrofit}" + implementation "com.squareup.retrofit2:converter-gson:${versions.retrofit}" + + implementation 'com.github.jetradarmobile:android-snowfall:1.2.0' } repositories { mavenCentral() diff --git a/app/libs/java-json.jar b/app/libs/java-json.jar new file mode 100644 index 00000000..2f211e36 Binary files /dev/null and b/app/libs/java-json.jar differ diff --git a/app/proguard/app.pro b/app/proguard/app.pro index 162b8fa9..0c00ff11 100644 --- a/app/proguard/app.pro +++ b/app/proguard/app.pro @@ -24,6 +24,7 @@ -keep class pl.szczodrzynski.edziennik.utils.models.** { *; } -keep class pl.szczodrzynski.edziennik.data.db.modules.events.Event { *; } -keep class pl.szczodrzynski.edziennik.data.db.modules.events.EventFull { *; } +-keep class pl.szczodrzynski.edziennik.ui.modules.home.HomeCardModel { *; } -keepclassmembers class pl.szczodrzynski.edziennik.widgets.WidgetConfig { public *; } -keepnames class pl.szczodrzynski.edziennik.WidgetTimetable -keepnames class pl.szczodrzynski.edziennik.notifications.WidgetNotifications @@ -39,4 +40,22 @@ -keep class okhttp3.** { *; } --keep class com.google.android.material.tabs.** {*;} \ No newline at end of file +-keep class com.google.android.material.tabs.** {*;} + +# ServiceLoader support + -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} + +# Most of volatile fields are updated with AFU and should not be mangled +-keepclassmembernames class kotlinx.** { + volatile ; +} + +-keepclasseswithmembernames class * { + native ; +} + +-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.** { *; } \ No newline at end of file diff --git a/app/sampledata/check/ic_check.xml b/app/sampledata/check/ic_check.xml new file mode 100644 index 00000000..f621023c --- /dev/null +++ b/app/sampledata/check/ic_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/sampledata/format-bold/ic_format_bold.xml b/app/sampledata/format-bold/ic_format_bold.xml new file mode 100644 index 00000000..87bc9819 --- /dev/null +++ b/app/sampledata/format-bold/ic_format_bold.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/sampledata/format/ic_format_italic.xml b/app/sampledata/format/ic_format_italic.xml new file mode 100644 index 00000000..b44be3bb --- /dev/null +++ b/app/sampledata/format/ic_format_italic.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/sampledata/format/ic_format_underline.xml b/app/sampledata/format/ic_format_underline.xml new file mode 100644 index 00000000..e3d40e63 --- /dev/null +++ b/app/sampledata/format/ic_format_underline.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/sampledata/settings/ic_settings.xml b/app/sampledata/settings/ic_settings.xml new file mode 100644 index 00000000..efaaa4ee --- /dev/null +++ b/app/sampledata/settings/ic_settings.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 991589de..4300d4cc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,13 +12,17 @@ android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/SplashTheme" + android:theme="@style/AppTheme.Dark" android:usesCleartextTraffic="true" tools:ignore="UnusedAttribute"> + @@ -28,18 +32,13 @@ - @@ -95,28 +94,26 @@ __/ | |_ --> - - - - - + @@ -169,7 +166,6 @@ android:name="android.appwidget.provider" android:resource="@xml/widget_notifications_info" /> - @@ -188,7 +184,6 @@ - @@ -196,6 +191,24 @@ + + + + + + + + + + + + + @@ -214,10 +227,7 @@ - + diff --git a/app/src/main/assets/pl-changelog.html b/app/src/main/assets/pl-changelog.html index eab231b6..18f185d1 100644 --- a/app/src/main/assets/pl-changelog.html +++ b/app/src/main/assets/pl-changelog.html @@ -1,99 +1,38 @@ - - - - - - - -

Wersja 3.1.1, 2019-10-09

+

Wersja 4.0-beta.1, 2020-01-02

    -
  • Librus: poprawiona synchronizacja kategorii i kolorów ocen.
  • -
  • Zmieniony kolor dolnego paska w ciemnym motywie.
  • -
  • Zaktualizowany licznik czasu lekcji.
  • +
  • Przebudowaliśmy cały moduł synchronizacji, co oznacza większą stabilność aplikacji, szybkosć oraz poprawność pobieranych danych.
  • +
  • Wysyłanie wiadomości - funkcja, na którą czekał każdy. Od teraz w Szkolnym można wysyłać oraz odpowiadać na wiadomości do nauczycieli 👏
  • +
  • Udoskonalony wygląd Szkolnego - sprawi, że korzystanie z aplikacji będzie jeszcze przyjemniejsze
  • +
  • Nowa Strona główna - ładniejszy wygląd oraz możliwość przestawiania kart na każdym profilu
  • +
  • Nowy Plan lekcji - z doskonałą obsługą lekcji przesuniętych oraz dwóch lekcji o tej samej godzinie
  • +
  • Nowe okienka informacji o wydarzeniach oraz lekcjach
  • +
  • Łatwiejsze dodawanie własnych wydarzeń
  • +
  • Dużo poprawek w widoku Wiadomości oraz Ogłoszeń
  • +
  • Częściowa Obsługa dziennika EduDziennik
  • +
  • Librus: opcja logowania w dziennikach Jednostek Samorządu Terytorialnego oraz Oświata w Radomiu
  • +
  • Librus: obsługa Zadań domowych bez posiadania Mobilnych dodatków (przez system Synergia)
  • +
  • Lepsze przekazywanie powiadomień na komputer oraz łatwiejsze parowanie
  • +
  • Poprawiliśmy synchronizację w tle na niektórych telefonach
  • +
  • Znaczna ilość błędów z poprzednich wersji już nie występuje
- -

Wersja 3.1, 2019-09-29

+
+
+
+Uwaga. Ponieważ to wersja beta, niektóre funkcje mogą nie działać prawidłowo.
+Staramy się usuwać takie przypadki, jednak na chwilę obecną mogą występować błędy w:
    -
  • Poprawiony interfejs zadań domowych.
  • -
  • Librus: wyświetlanie komentarzy ocen.
  • -
  • Librus: wyświetlanie nieobecności nauczycieli w Terminarzu.
  • -
  • Librus: usprawniona synchronizacja ocen.
  • -
  • Poprawki angielskiego tłumaczenia.
  • +
  • Wysyłanie wiadomości może czasami nie działać - proszę o zgłaszanie wszystkich błędów na naszym serwerze Discord
  • +
  • Widget powiadomień
  • +
  • Terminarz - brak informacji o odwołanych lekcjach w dialogu
  • +
  • Brak generowania blokowego planu lekcji
- -

Wersja 3.0.3, 2019-09-26

-
    -
  • Librus: poprawka kilku błędów synchronizacji.
  • -
  • Vulcan: prawidłowe oznaczanie wiadomości jako przeczytana.
  • -
  • Vulcan: poprawiona synchronizacja wiadomości i frekwencji.
  • -
  • Vulcan: poprawka błędów logowania.
  • -
- -

Wersja 3.0.2, 2019-09-24

-
    -
  • Librus: pobieranie Bieżących ocen opisowych.
  • -
  • Poprawki UI: kolor ikon paska statusu w jasnym motywie.
  • -
  • Poprawka braku skanera QR do przekazywania powiadomień.
  • -
  • Poprawka wyboru koloru i daty własnego wydarzenia, które crashowały aplikację.
  • -
- -

Wersja 3.0.1, 2019-09-19

-
    -
  • Librus: Poprawa błędu synchronizacji.
  • -
  • Poprawki UI związane z paskiem nawigacji.
  • -
  • Mobidziennik: Pobieranie ocen w niektórych przedmiotach.
  • -
- -

Wersja 3.0, 2019-09-13

-
    -
  • Nowy wygląd i sposób nawigacji w całej aplikacji.
  • -
  • Menu nawigacji można teraz otworzyć przyciskiem na dolnym pasku. Pociągnięcie w górę tego paska wyświetla menu kontekstowe dotyczące danego widoku.
  • -
  • Założyliśmy serwer Discord! https://discord.gg/n9e8pWr
  • -
    -
  • Librus: poprawka powielonych ogłoszeń szkolnych.
  • -
  • Naprawiłem błąd nieskończonej synchronizacji w Vulcanie.
  • -
  • Naprawiłem crash launchera przy dodaniu widgetu.
  • -
  • Naprawiłem częste crashe związane z widokiem kalendarza.
  • -
  • Nowe, ładniejsze (choć trochę) motywy kolorów.
  • -
  • Dużo drobnych poprawek UI i działania aplikacji.
  • -
- - - - \ No newline at end of file +
+
+
+
+Okazja ograniczona czasowo: Poczuj prawdziwą zimę, włączając w Ustawieniach widok padającego śniegu! +
+
+
+Dzięki za korzystanie ze Szkolnego!
+© Kuba Szczodrzyński, Kacper Ziubryniewicz 2020 diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..e8096e62 --- /dev/null +++ b/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,44 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html + +# Sets the minimum version of CMake required to build the native library. + +cmake_minimum_required(VERSION 3.4.1) + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. + +add_library( # Sets the name of the library. + szkolny-signing + + # Sets the library as a shared library. + SHARED + + # Provides a relative path to your source file(s). + szkolny-signing.cpp) + +# Searches for a specified prebuilt library and stores the path as a +# variable. Because CMake includes system libraries in the search path by +# default, you only need to specify the name of the public NDK library +# you want to add. CMake verifies that the library exists before +# completing its build. + +#[[find_library( # Sets the name of the path variable. + log-lib + + # Specifies the name of the NDK library that + # you want CMake to locate. + log )]] + +# Specifies libraries CMake should link to your target library. You +# can link multiple libraries, such as libraries you define in this +# build script, prebuilt third-party libraries, or system libraries. + +target_link_libraries( # Specifies the target library. + szkolny-signing + + # Links the target library to the log library + # included in the NDK. + ${log-lib} ) \ No newline at end of file diff --git a/app/src/main/cpp/aes.c b/app/src/main/cpp/aes.c new file mode 100644 index 00000000..c67bc03f --- /dev/null +++ b/app/src/main/cpp/aes.c @@ -0,0 +1,520 @@ +#include +#include +#include "aes.h" + +#include + +#define airport(x) (((x) << 8) | ((x) >> 24)) + +#define TRUE 1 +#define FALSE 0 + +static const toys wtf[16][16] = { + {0x63,0x7C,0x77,0x7B,0xF2,0x6B,0x6F,0xC5,0x30,0x01,0x67,0x2B,0xFE,0xD7,0xAB,0x76}, + {0xCA,0x82,0xC9,0x7D,0xFA,0x59,0x47,0xF0,0xAD,0xD4,0xA2,0xAF,0x9C,0xA4,0x72,0xC0}, + {0xB7,0xFD,0x93,0x26,0x36,0x3F,0xF7,0xCC,0x34,0xA5,0xE5,0xF1,0x71,0xD8,0x31,0x15}, + {0x04,0xC7,0x23,0xC3,0x18,0x96,0x05,0x9A,0x07,0x12,0x80,0xE2,0xEB,0x27,0xB2,0x75}, + {0x09,0x83,0x2C,0x1A,0x1B,0x6E,0x5A,0xA0,0x52,0x3B,0xD6,0xB3,0x29,0xE3,0x2F,0x84}, + {0x53,0xD1,0x00,0xED,0x20,0xFC,0xB1,0x5B,0x6A,0xCB,0xBE,0x39,0x4A,0x4C,0x58,0xCF}, + {0xD0,0xEF,0xAA,0xFB,0x43,0x4D,0x33,0x85,0x45,0xF9,0x02,0x7F,0x50,0x3C,0x9F,0xA8}, + {0x51,0xA3,0x40,0x8F,0x92,0x9D,0x38,0xF5,0xBC,0xB6,0xDA,0x21,0x10,0xFF,0xF3,0xD2}, + {0xCD,0x0C,0x13,0xEC,0x5F,0x97,0x44,0x17,0xC4,0xA7,0x7E,0x3D,0x64,0x5D,0x19,0x73}, + {0x60,0x81,0x4F,0xDC,0x22,0x2A,0x90,0x88,0x46,0xEE,0xB8,0x14,0xDE,0x5E,0x0B,0xDB}, + {0xE0,0x32,0x3A,0x0A,0x49,0x06,0x24,0x5C,0xC2,0xD3,0xAC,0x62,0x91,0x95,0xE4,0x79}, + {0xE7,0xC8,0x37,0x6D,0x8D,0xD5,0x4E,0xA9,0x6C,0x56,0xF4,0xEA,0x65,0x7A,0xAE,0x08}, + {0xBA,0x78,0x25,0x2E,0x1C,0xA6,0xB4,0xC6,0xE8,0xDD,0x74,0x1F,0x4B,0xBD,0x8B,0x8A}, + {0x70,0x3E,0xB5,0x66,0x48,0x03,0xF6,0x0E,0x61,0x35,0x57,0xB9,0x86,0xC1,0x1D,0x9E}, + {0xE1,0xF8,0x98,0x11,0x69,0xD9,0x8E,0x94,0x9B,0x1E,0x87,0xE9,0xCE,0x55,0x28,0xDF}, + {0x8C,0xA1,0x89,0x0D,0xBF,0xE6,0x42,0x68,0x41,0x99,0x2D,0x0F,0xB0,0x54,0xBB,0x16} +}; + +static const toys help_me[256][6] = { + {0x00,0x00,0x00,0x00,0x00,0x00},{0x02,0x03,0x09,0x0b,0x0d,0x0e}, + {0x04,0x06,0x12,0x16,0x1a,0x1c},{0x06,0x05,0x1b,0x1d,0x17,0x12}, + {0x08,0x0c,0x24,0x2c,0x34,0x38},{0x0a,0x0f,0x2d,0x27,0x39,0x36}, + {0x0c,0x0a,0x36,0x3a,0x2e,0x24},{0x0e,0x09,0x3f,0x31,0x23,0x2a}, + {0x10,0x18,0x48,0x58,0x68,0x70},{0x12,0x1b,0x41,0x53,0x65,0x7e}, + {0x14,0x1e,0x5a,0x4e,0x72,0x6c},{0x16,0x1d,0x53,0x45,0x7f,0x62}, + {0x18,0x14,0x6c,0x74,0x5c,0x48},{0x1a,0x17,0x65,0x7f,0x51,0x46}, + {0x1c,0x12,0x7e,0x62,0x46,0x54},{0x1e,0x11,0x77,0x69,0x4b,0x5a}, + {0x20,0x30,0x90,0xb0,0xd0,0xe0},{0x22,0x33,0x99,0xbb,0xdd,0xee}, + {0x24,0x36,0x82,0xa6,0xca,0xfc},{0x26,0x35,0x8b,0xad,0xc7,0xf2}, + {0x28,0x3c,0xb4,0x9c,0xe4,0xd8},{0x2a,0x3f,0xbd,0x97,0xe9,0xd6}, + {0x2c,0x3a,0xa6,0x8a,0xfe,0xc4},{0x2e,0x39,0xaf,0x81,0xf3,0xca}, + {0x30,0x28,0xd8,0xe8,0xb8,0x90},{0x32,0x2b,0xd1,0xe3,0xb5,0x9e}, + {0x34,0x2e,0xca,0xfe,0xa2,0x8c},{0x36,0x2d,0xc3,0xf5,0xaf,0x82}, + {0x38,0x24,0xfc,0xc4,0x8c,0xa8},{0x3a,0x27,0xf5,0xcf,0x81,0xa6}, + {0x3c,0x22,0xee,0xd2,0x96,0xb4},{0x3e,0x21,0xe7,0xd9,0x9b,0xba}, + {0x40,0x60,0x3b,0x7b,0xbb,0xdb},{0x42,0x63,0x32,0x70,0xb6,0xd5}, + {0x44,0x66,0x29,0x6d,0xa1,0xc7},{0x46,0x65,0x20,0x66,0xac,0xc9}, + {0x48,0x6c,0x1f,0x57,0x8f,0xe3},{0x4a,0x6f,0x16,0x5c,0x82,0xed}, + {0x4c,0x6a,0x0d,0x41,0x95,0xff},{0x4e,0x69,0x04,0x4a,0x98,0xf1}, + {0x50,0x78,0x73,0x23,0xd3,0xab},{0x52,0x7b,0x7a,0x28,0xde,0xa5}, + {0x54,0x7e,0x61,0x35,0xc9,0xb7},{0x56,0x7d,0x68,0x3e,0xc4,0xb9}, + {0x58,0x74,0x57,0x0f,0xe7,0x93},{0x5a,0x77,0x5e,0x04,0xea,0x9d}, + {0x5c,0x72,0x45,0x19,0xfd,0x8f},{0x5e,0x71,0x4c,0x12,0xf0,0x81}, + {0x60,0x50,0xab,0xcb,0x6b,0x3b},{0x62,0x53,0xa2,0xc0,0x66,0x35}, + {0x64,0x56,0xb9,0xdd,0x71,0x27},{0x66,0x55,0xb0,0xd6,0x7c,0x29}, + {0x68,0x5c,0x8f,0xe7,0x5f,0x03},{0x6a,0x5f,0x86,0xec,0x52,0x0d}, + {0x6c,0x5a,0x9d,0xf1,0x45,0x1f},{0x6e,0x59,0x94,0xfa,0x48,0x11}, + {0x70,0x48,0xe3,0x93,0x03,0x4b},{0x72,0x4b,0xea,0x98,0x0e,0x45}, + {0x74,0x4e,0xf1,0x85,0x19,0x57},{0x76,0x4d,0xf8,0x8e,0x14,0x59}, + {0x78,0x44,0xc7,0xbf,0x37,0x73},{0x7a,0x47,0xce,0xb4,0x3a,0x7d}, + {0x7c,0x42,0xd5,0xa9,0x2d,0x6f},{0x7e,0x41,0xdc,0xa2,0x20,0x61}, + {0x80,0xc0,0x76,0xf6,0x6d,0xad},{0x82,0xc3,0x7f,0xfd,0x60,0xa3}, + {0x84,0xc6,0x64,0xe0,0x77,0xb1},{0x86,0xc5,0x6d,0xeb,0x7a,0xbf}, + {0x88,0xcc,0x52,0xda,0x59,0x95},{0x8a,0xcf,0x5b,0xd1,0x54,0x9b}, + {0x8c,0xca,0x40,0xcc,0x43,0x89},{0x8e,0xc9,0x49,0xc7,0x4e,0x87}, + {0x90,0xd8,0x3e,0xae,0x05,0xdd},{0x92,0xdb,0x37,0xa5,0x08,0xd3}, + {0x94,0xde,0x2c,0xb8,0x1f,0xc1},{0x96,0xdd,0x25,0xb3,0x12,0xcf}, + {0x98,0xd4,0x1a,0x82,0x31,0xe5},{0x9a,0xd7,0x13,0x89,0x3c,0xeb}, + {0x9c,0xd2,0x08,0x94,0x2b,0xf9},{0x9e,0xd1,0x01,0x9f,0x26,0xf7}, + {0xa0,0xf0,0xe6,0x46,0xbd,0x4d},{0xa2,0xf3,0xef,0x4d,0xb0,0x43}, + {0xa4,0xf6,0xf4,0x50,0xa7,0x51},{0xa6,0xf5,0xfd,0x5b,0xaa,0x5f}, + {0xa8,0xfc,0xc2,0x6a,0x89,0x75},{0xaa,0xff,0xcb,0x61,0x84,0x7b}, + {0xac,0xfa,0xd0,0x7c,0x93,0x69},{0xae,0xf9,0xd9,0x77,0x9e,0x67}, + {0xb0,0xe8,0xae,0x1e,0xd5,0x3d},{0xb2,0xeb,0xa7,0x15,0xd8,0x33}, + {0xb4,0xee,0xbc,0x08,0xcf,0x21},{0xb6,0xed,0xb5,0x03,0xc2,0x2f}, + {0xb8,0xe4,0x8a,0x32,0xe1,0x05},{0xba,0xe7,0x83,0x39,0xec,0x0b}, + {0xbc,0xe2,0x98,0x24,0xfb,0x19},{0xbe,0xe1,0x91,0x2f,0xf6,0x17}, + {0xc0,0xa0,0x4d,0x8d,0xd6,0x76},{0xc2,0xa3,0x44,0x86,0xdb,0x78}, + {0xc4,0xa6,0x5f,0x9b,0xcc,0x6a},{0xc6,0xa5,0x56,0x90,0xc1,0x64}, + {0xc8,0xac,0x69,0xa1,0xe2,0x4e},{0xca,0xaf,0x60,0xaa,0xef,0x40}, + {0xcc,0xaa,0x7b,0xb7,0xf8,0x52},{0xce,0xa9,0x72,0xbc,0xf5,0x5c}, + {0xd0,0xb8,0x05,0xd5,0xbe,0x06},{0xd2,0xbb,0x0c,0xde,0xb3,0x08}, + {0xd4,0xbe,0x17,0xc3,0xa4,0x1a},{0xd6,0xbd,0x1e,0xc8,0xa9,0x14}, + {0xd8,0xb4,0x21,0xf9,0x8a,0x3e},{0xda,0xb7,0x28,0xf2,0x87,0x30}, + {0xdc,0xb2,0x33,0xef,0x90,0x22},{0xde,0xb1,0x3a,0xe4,0x9d,0x2c}, + {0xe0,0x90,0xdd,0x3d,0x06,0x96},{0xe2,0x93,0xd4,0x36,0x0b,0x98}, + {0xe4,0x96,0xcf,0x2b,0x1c,0x8a},{0xe6,0x95,0xc6,0x20,0x11,0x84}, + {0xe8,0x9c,0xf9,0x11,0x32,0xae},{0xea,0x9f,0xf0,0x1a,0x3f,0xa0}, + {0xec,0x9a,0xeb,0x07,0x28,0xb2},{0xee,0x99,0xe2,0x0c,0x25,0xbc}, + {0xf0,0x88,0x95,0x65,0x6e,0xe6},{0xf2,0x8b,0x9c,0x6e,0x63,0xe8}, + {0xf4,0x8e,0x87,0x73,0x74,0xfa},{0xf6,0x8d,0x8e,0x78,0x79,0xf4}, + {0xf8,0x84,0xb1,0x49,0x5a,0xde},{0xfa,0x87,0xb8,0x42,0x57,0xd0}, + {0xfc,0x82,0xa3,0x5f,0x40,0xc2},{0xfe,0x81,0xaa,0x54,0x4d,0xcc}, + {0x1b,0x9b,0xec,0xf7,0xda,0x41},{0x19,0x98,0xe5,0xfc,0xd7,0x4f}, + {0x1f,0x9d,0xfe,0xe1,0xc0,0x5d},{0x1d,0x9e,0xf7,0xea,0xcd,0x53}, + {0x13,0x97,0xc8,0xdb,0xee,0x79},{0x11,0x94,0xc1,0xd0,0xe3,0x77}, + {0x17,0x91,0xda,0xcd,0xf4,0x65},{0x15,0x92,0xd3,0xc6,0xf9,0x6b}, + {0x0b,0x83,0xa4,0xaf,0xb2,0x31},{0x09,0x80,0xad,0xa4,0xbf,0x3f}, + {0x0f,0x85,0xb6,0xb9,0xa8,0x2d},{0x0d,0x86,0xbf,0xb2,0xa5,0x23}, + {0x03,0x8f,0x80,0x83,0x86,0x09},{0x01,0x8c,0x89,0x88,0x8b,0x07}, + {0x07,0x89,0x92,0x95,0x9c,0x15},{0x05,0x8a,0x9b,0x9e,0x91,0x1b}, + {0x3b,0xab,0x7c,0x47,0x0a,0xa1},{0x39,0xa8,0x75,0x4c,0x07,0xaf}, + {0x3f,0xad,0x6e,0x51,0x10,0xbd},{0x3d,0xae,0x67,0x5a,0x1d,0xb3}, + {0x33,0xa7,0x58,0x6b,0x3e,0x99},{0x31,0xa4,0x51,0x60,0x33,0x97}, + {0x37,0xa1,0x4a,0x7d,0x24,0x85},{0x35,0xa2,0x43,0x76,0x29,0x8b}, + {0x2b,0xb3,0x34,0x1f,0x62,0xd1},{0x29,0xb0,0x3d,0x14,0x6f,0xdf}, + {0x2f,0xb5,0x26,0x09,0x78,0xcd},{0x2d,0xb6,0x2f,0x02,0x75,0xc3}, + {0x23,0xbf,0x10,0x33,0x56,0xe9},{0x21,0xbc,0x19,0x38,0x5b,0xe7}, + {0x27,0xb9,0x02,0x25,0x4c,0xf5},{0x25,0xba,0x0b,0x2e,0x41,0xfb}, + {0x5b,0xfb,0xd7,0x8c,0x61,0x9a},{0x59,0xf8,0xde,0x87,0x6c,0x94}, + {0x5f,0xfd,0xc5,0x9a,0x7b,0x86},{0x5d,0xfe,0xcc,0x91,0x76,0x88}, + {0x53,0xf7,0xf3,0xa0,0x55,0xa2},{0x51,0xf4,0xfa,0xab,0x58,0xac}, + {0x57,0xf1,0xe1,0xb6,0x4f,0xbe},{0x55,0xf2,0xe8,0xbd,0x42,0xb0}, + {0x4b,0xe3,0x9f,0xd4,0x09,0xea},{0x49,0xe0,0x96,0xdf,0x04,0xe4}, + {0x4f,0xe5,0x8d,0xc2,0x13,0xf6},{0x4d,0xe6,0x84,0xc9,0x1e,0xf8}, + {0x43,0xef,0xbb,0xf8,0x3d,0xd2},{0x41,0xec,0xb2,0xf3,0x30,0xdc}, + {0x47,0xe9,0xa9,0xee,0x27,0xce},{0x45,0xea,0xa0,0xe5,0x2a,0xc0}, + {0x7b,0xcb,0x47,0x3c,0xb1,0x7a},{0x79,0xc8,0x4e,0x37,0xbc,0x74}, + {0x7f,0xcd,0x55,0x2a,0xab,0x66},{0x7d,0xce,0x5c,0x21,0xa6,0x68}, + {0x73,0xc7,0x63,0x10,0x85,0x42},{0x71,0xc4,0x6a,0x1b,0x88,0x4c}, + {0x77,0xc1,0x71,0x06,0x9f,0x5e},{0x75,0xc2,0x78,0x0d,0x92,0x50}, + {0x6b,0xd3,0x0f,0x64,0xd9,0x0a},{0x69,0xd0,0x06,0x6f,0xd4,0x04}, + {0x6f,0xd5,0x1d,0x72,0xc3,0x16},{0x6d,0xd6,0x14,0x79,0xce,0x18}, + {0x63,0xdf,0x2b,0x48,0xed,0x32},{0x61,0xdc,0x22,0x43,0xe0,0x3c}, + {0x67,0xd9,0x39,0x5e,0xf7,0x2e},{0x65,0xda,0x30,0x55,0xfa,0x20}, + {0x9b,0x5b,0x9a,0x01,0xb7,0xec},{0x99,0x58,0x93,0x0a,0xba,0xe2}, + {0x9f,0x5d,0x88,0x17,0xad,0xf0},{0x9d,0x5e,0x81,0x1c,0xa0,0xfe}, + {0x93,0x57,0xbe,0x2d,0x83,0xd4},{0x91,0x54,0xb7,0x26,0x8e,0xda}, + {0x97,0x51,0xac,0x3b,0x99,0xc8},{0x95,0x52,0xa5,0x30,0x94,0xc6}, + {0x8b,0x43,0xd2,0x59,0xdf,0x9c},{0x89,0x40,0xdb,0x52,0xd2,0x92}, + {0x8f,0x45,0xc0,0x4f,0xc5,0x80},{0x8d,0x46,0xc9,0x44,0xc8,0x8e}, + {0x83,0x4f,0xf6,0x75,0xeb,0xa4},{0x81,0x4c,0xff,0x7e,0xe6,0xaa}, + {0x87,0x49,0xe4,0x63,0xf1,0xb8},{0x85,0x4a,0xed,0x68,0xfc,0xb6}, + {0xbb,0x6b,0x0a,0xb1,0x67,0x0c},{0xb9,0x68,0x03,0xba,0x6a,0x02}, + {0xbf,0x6d,0x18,0xa7,0x7d,0x10},{0xbd,0x6e,0x11,0xac,0x70,0x1e}, + {0xb3,0x67,0x2e,0x9d,0x53,0x34},{0xb1,0x64,0x27,0x96,0x5e,0x3a}, + {0xb7,0x61,0x3c,0x8b,0x49,0x28},{0xb5,0x62,0x35,0x80,0x44,0x26}, + {0xab,0x73,0x42,0xe9,0x0f,0x7c},{0xa9,0x70,0x4b,0xe2,0x02,0x72}, + {0xaf,0x75,0x50,0xff,0x15,0x60},{0xad,0x76,0x59,0xf4,0x18,0x6e}, + {0xa3,0x7f,0x66,0xc5,0x3b,0x44},{0xa1,0x7c,0x6f,0xce,0x36,0x4a}, + {0xa7,0x79,0x74,0xd3,0x21,0x58},{0xa5,0x7a,0x7d,0xd8,0x2c,0x56}, + {0xdb,0x3b,0xa1,0x7a,0x0c,0x37},{0xd9,0x38,0xa8,0x71,0x01,0x39}, + {0xdf,0x3d,0xb3,0x6c,0x16,0x2b},{0xdd,0x3e,0xba,0x67,0x1b,0x25}, + {0xd3,0x37,0x85,0x56,0x38,0x0f},{0xd1,0x34,0x8c,0x5d,0x35,0x01}, + {0xd7,0x31,0x97,0x40,0x22,0x13},{0xd5,0x32,0x9e,0x4b,0x2f,0x1d}, + {0xcb,0x23,0xe9,0x22,0x64,0x47},{0xc9,0x20,0xe0,0x29,0x69,0x49}, + {0xcf,0x25,0xfb,0x34,0x7e,0x5b},{0xcd,0x26,0xf2,0x3f,0x73,0x55}, + {0xc3,0x2f,0xcd,0x0e,0x50,0x7f},{0xc1,0x2c,0xc4,0x05,0x5d,0x71}, + {0xc7,0x29,0xdf,0x18,0x4a,0x63},{0xc5,0x2a,0xd6,0x13,0x47,0x6d}, + {0xfb,0x0b,0x31,0xca,0xdc,0xd7},{0xf9,0x08,0x38,0xc1,0xd1,0xd9}, + {0xff,0x0d,0x23,0xdc,0xc6,0xcb},{0xfd,0x0e,0x2a,0xd7,0xcb,0xc5}, + {0xf3,0x07,0x15,0xe6,0xe8,0xef},{0xf1,0x04,0x1c,0xed,0xe5,0xe1}, + {0xf7,0x01,0x07,0xf0,0xf2,0xf3},{0xf5,0x02,0x0e,0xfb,0xff,0xfd}, + {0xeb,0x13,0x79,0x92,0xb4,0xa7},{0xe9,0x10,0x70,0x99,0xb9,0xa9}, + {0xef,0x15,0x6b,0x84,0xae,0xbb},{0xed,0x16,0x62,0x8f,0xa3,0xb5}, + {0xe3,0x1f,0x5d,0xbe,0x80,0x9f},{0xe1,0x1c,0x54,0xb5,0x8d,0x91}, + {0xe7,0x19,0x4f,0xa8,0x9a,0x83},{0xe5,0x1a,0x46,0xa3,0x97,0x8d} +}; + +void throat(const toys *decide, toys *selection, size_t plane) +{ + size_t idx; + + for (idx = 0; idx < plane; idx++) + selection[idx] ^= decide[idx]; +} + +int death(const toys *squirrel, size_t dear, toys *awful, const arrest *wrong, int magic, const toys *fit) +{ + toys supermarket[calculator], software[calculator], bookstore[calculator]; + int brainwash, jellybean; + + if (dear % calculator != 0) + return(FALSE); + + brainwash = dear / calculator; + + memcpy(bookstore, fit, calculator); + + for (jellybean = 0; jellybean < brainwash; jellybean++) { + memcpy(supermarket, &squirrel[jellybean * calculator], calculator); + throat(bookstore, supermarket, calculator); + punishment(supermarket, software, wrong, magic); + memcpy(&awful[jellybean * calculator], software, calculator); + memcpy(bookstore, software, calculator); + } + + return(TRUE); +} + +arrest please(arrest lol) +{ + unsigned int apps; + + apps = (int)wtf[(lol >> 4) & 0x0000000F][lol & 0x0000000F]; + apps += (int)wtf[(lol >> 12) & 0x0000000F][(lol >> 8) & 0x0000000F] << 8; + apps += (int)wtf[(lol >> 20) & 0x0000000F][(lol >> 16) & 0x0000000F] << 16; + apps += (int)wtf[(lol >> 28) & 0x0000000F][(lol >> 24) & 0x0000000F] << 24; + return(apps); +} + +void stoprightnow(const toys *fuckoff, arrest *waste, int roadtrip) +{ + int tour=4,cause,lively,reply; + arrest nope,desert[]={0x01000000, 0x02000000, 0x04000000, 0x08000000, 0x10000000, 0x20000000, + 0x40000000, 0x80000000, 0x1b000000, 0x36000000, 0x6c000000, 0xd8000000, + 0xab000000, 0x4d000000, 0x9a000000}; + + switch (roadtrip) { + case 128: cause = 10; lively = 4; break; + case 192: cause = 12; lively = 6; break; + case 256: cause = 14; lively = 8; break; + default: return; + } + + for (reply=0; reply < lively; ++reply) { + waste[reply] = ((fuckoff[4 * reply]) << 24) | ((fuckoff[4 * reply + 1]) << 16) | + ((fuckoff[4 * reply + 2]) << 8) | ((fuckoff[4 * reply + 3])); + } + + for (reply = lively; reply < tour * (cause + 1); ++reply) { + nope = waste[reply - 1]; + if ((reply % lively) == 0) + nope = please(airport(nope)) ^ desert[(reply - 1) / lively]; + else if (lively > 6 && (reply % lively) == 4) + nope = please(nope); + waste[reply] = waste[reply - lively] ^ nope; + } +} + +void hot(toys rare[][4], const arrest powerful[]) +{ + toys interfere[4]; + + // memcpy(interfere,&powerful[idx],4); // Not accurate for big endian machines + // Subkey 1 + interfere[0] = powerful[0] >> 24; + interfere[1] = powerful[0] >> 16; + interfere[2] = powerful[0] >> 8; + interfere[3] = powerful[0]; + rare[0][0] ^= interfere[0]; + rare[1][0] ^= interfere[1]; + rare[2][0] ^= interfere[2]; + rare[3][0] ^= interfere[3]; + // Subkey 2 + interfere[0] = powerful[1] >> 24; + interfere[1] = powerful[1] >> 16; + interfere[2] = powerful[1] >> 8; + interfere[3] = powerful[1]; + rare[0][1] ^= interfere[0]; + rare[1][1] ^= interfere[1]; + rare[2][1] ^= interfere[2]; + rare[3][1] ^= interfere[3]; + // Subkey 3 + interfere[0] = powerful[2] >> 24; + interfere[1] = powerful[2] >> 16; + interfere[2] = powerful[2] >> 8; + interfere[3] = powerful[2]; + rare[0][2] ^= interfere[0]; + rare[1][2] ^= interfere[1]; + rare[2][2] ^= interfere[2]; + rare[3][2] ^= interfere[3]; + // Subkey 4 + interfere[0] = powerful[3] >> 24; + interfere[1] = powerful[3] >> 16; + interfere[2] = powerful[3] >> 8; + interfere[3] = powerful[3]; + rare[0][3] ^= interfere[0]; + rare[1][3] ^= interfere[1]; + rare[2][3] ^= interfere[2]; + rare[3][3] ^= interfere[3]; +} + +void numerous(toys vigorous[][4]) +{ + vigorous[0][0] = wtf[vigorous[0][0] >> 4][vigorous[0][0] & 0x0F]; + vigorous[0][1] = wtf[vigorous[0][1] >> 4][vigorous[0][1] & 0x0F]; + vigorous[0][2] = wtf[vigorous[0][2] >> 4][vigorous[0][2] & 0x0F]; + vigorous[0][3] = wtf[vigorous[0][3] >> 4][vigorous[0][3] & 0x0F]; + vigorous[1][0] = wtf[vigorous[1][0] >> 4][vigorous[1][0] & 0x0F]; + vigorous[1][1] = wtf[vigorous[1][1] >> 4][vigorous[1][1] & 0x0F]; + vigorous[1][2] = wtf[vigorous[1][2] >> 4][vigorous[1][2] & 0x0F]; + vigorous[1][3] = wtf[vigorous[1][3] >> 4][vigorous[1][3] & 0x0F]; + vigorous[2][0] = wtf[vigorous[2][0] >> 4][vigorous[2][0] & 0x0F]; + vigorous[2][1] = wtf[vigorous[2][1] >> 4][vigorous[2][1] & 0x0F]; + vigorous[2][2] = wtf[vigorous[2][2] >> 4][vigorous[2][2] & 0x0F]; + vigorous[2][3] = wtf[vigorous[2][3] >> 4][vigorous[2][3] & 0x0F]; + vigorous[3][0] = wtf[vigorous[3][0] >> 4][vigorous[3][0] & 0x0F]; + vigorous[3][1] = wtf[vigorous[3][1] >> 4][vigorous[3][1] & 0x0F]; + vigorous[3][2] = wtf[vigorous[3][2] >> 4][vigorous[3][2] & 0x0F]; + vigorous[3][3] = wtf[vigorous[3][3] >> 4][vigorous[3][3] & 0x0F]; +} + +void crowded(toys chalk[][4]) +{ + int t; + + // Shift left by 1 + t = chalk[1][0]; + chalk[1][0] = chalk[1][1]; + chalk[1][1] = chalk[1][2]; + chalk[1][2] = chalk[1][3]; + chalk[1][3] = t; + // Shift left by 2 + t = chalk[2][0]; + chalk[2][0] = chalk[2][2]; + chalk[2][2] = t; + t = chalk[2][1]; + chalk[2][1] = chalk[2][3]; + chalk[2][3] = t; + // Shift left by 3 + t = chalk[3][0]; + chalk[3][0] = chalk[3][3]; + chalk[3][3] = chalk[3][2]; + chalk[3][2] = chalk[3][1]; + chalk[3][1] = t; +} + +void scale(toys oh_no[][4]) +{ + toys idk[4]; + + // Column 1 + idk[0] = oh_no[0][0]; + idk[1] = oh_no[1][0]; + idk[2] = oh_no[2][0]; + idk[3] = oh_no[3][0]; + oh_no[0][0] = help_me[idk[0]][0]; + oh_no[0][0] ^= help_me[idk[1]][1]; + oh_no[0][0] ^= idk[2]; + oh_no[0][0] ^= idk[3]; + oh_no[1][0] = idk[0]; + oh_no[1][0] ^= help_me[idk[1]][0]; + oh_no[1][0] ^= help_me[idk[2]][1]; + oh_no[1][0] ^= idk[3]; + oh_no[2][0] = idk[0]; + oh_no[2][0] ^= idk[1]; + oh_no[2][0] ^= help_me[idk[2]][0]; + oh_no[2][0] ^= help_me[idk[3]][1]; + oh_no[3][0] = help_me[idk[0]][1]; + oh_no[3][0] ^= idk[1]; + oh_no[3][0] ^= idk[2]; + oh_no[3][0] ^= help_me[idk[3]][0]; + // Column 2 + idk[0] = oh_no[0][1]; + idk[1] = oh_no[1][1]; + idk[2] = oh_no[2][1]; + idk[3] = oh_no[3][1]; + oh_no[0][1] = help_me[idk[0]][0]; + oh_no[0][1] ^= help_me[idk[1]][1]; + oh_no[0][1] ^= idk[2]; + oh_no[0][1] ^= idk[3]; + oh_no[1][1] = idk[0]; + oh_no[1][1] ^= help_me[idk[1]][0]; + oh_no[1][1] ^= help_me[idk[2]][1]; + oh_no[1][1] ^= idk[3]; + oh_no[2][1] = idk[0]; + oh_no[2][1] ^= idk[1]; + oh_no[2][1] ^= help_me[idk[2]][0]; + oh_no[2][1] ^= help_me[idk[3]][1]; + oh_no[3][1] = help_me[idk[0]][1]; + oh_no[3][1] ^= idk[1]; + oh_no[3][1] ^= idk[2]; + oh_no[3][1] ^= help_me[idk[3]][0]; + // Column 3 + idk[0] = oh_no[0][2]; + idk[1] = oh_no[1][2]; + idk[2] = oh_no[2][2]; + idk[3] = oh_no[3][2]; + oh_no[0][2] = help_me[idk[0]][0]; + oh_no[0][2] ^= help_me[idk[1]][1]; + oh_no[0][2] ^= idk[2]; + oh_no[0][2] ^= idk[3]; + oh_no[1][2] = idk[0]; + oh_no[1][2] ^= help_me[idk[1]][0]; + oh_no[1][2] ^= help_me[idk[2]][1]; + oh_no[1][2] ^= idk[3]; + oh_no[2][2] = idk[0]; + oh_no[2][2] ^= idk[1]; + oh_no[2][2] ^= help_me[idk[2]][0]; + oh_no[2][2] ^= help_me[idk[3]][1]; + oh_no[3][2] = help_me[idk[0]][1]; + oh_no[3][2] ^= idk[1]; + oh_no[3][2] ^= idk[2]; + oh_no[3][2] ^= help_me[idk[3]][0]; + // Column 4 + idk[0] = oh_no[0][3]; + idk[1] = oh_no[1][3]; + idk[2] = oh_no[2][3]; + idk[3] = oh_no[3][3]; + oh_no[0][3] = help_me[idk[0]][0]; + oh_no[0][3] ^= help_me[idk[1]][1]; + oh_no[0][3] ^= idk[2]; + oh_no[0][3] ^= idk[3]; + oh_no[1][3] = idk[0]; + oh_no[1][3] ^= help_me[idk[1]][0]; + oh_no[1][3] ^= help_me[idk[2]][1]; + oh_no[1][3] ^= idk[3]; + oh_no[2][3] = idk[0]; + oh_no[2][3] ^= idk[1]; + oh_no[2][3] ^= help_me[idk[2]][0]; + oh_no[2][3] ^= help_me[idk[3]][1]; + oh_no[3][3] = help_me[idk[0]][1]; + oh_no[3][3] ^= idk[1]; + oh_no[3][3] ^= idk[2]; + oh_no[3][3] ^= help_me[idk[3]][0]; +} + +void punishment(const toys friends[], toys number[], const arrest wish[], int hang) +{ + toys burst[4][4]; + + burst[0][0] = friends[0]; + burst[1][0] = friends[1]; + burst[2][0] = friends[2]; + burst[3][0] = friends[3]; + burst[0][1] = friends[4]; + burst[1][1] = friends[5]; + burst[2][1] = friends[6]; + burst[3][1] = friends[7]; + burst[0][2] = friends[8]; + burst[1][2] = friends[9]; + burst[2][2] = friends[10]; + burst[3][2] = friends[11]; + burst[0][3] = friends[12]; + burst[1][3] = friends[13]; + burst[2][3] = friends[14]; + burst[3][3] = friends[15]; + + hot(burst, &wish[0]); + numerous(burst); + crowded(burst); + scale(burst); + hot(burst, &wish[4]); + numerous(burst); + crowded(burst); + scale(burst); + hot(burst, &wish[8]); + numerous(burst); + crowded(burst); + scale(burst); + hot(burst, &wish[12]); + numerous(burst); + crowded(burst); + scale(burst); + hot(burst, &wish[16]); + numerous(burst); + crowded(burst); + scale(burst); + hot(burst, &wish[20]); + numerous(burst); + crowded(burst); + scale(burst); + hot(burst, &wish[24]); + numerous(burst); + crowded(burst); + scale(burst); + hot(burst, &wish[28]); + numerous(burst); + crowded(burst); + scale(burst); + hot(burst, &wish[32]); + numerous(burst); + crowded(burst); + scale(burst); + hot(burst, &wish[36]); + if (hang != 128) { + numerous(burst); + crowded(burst); + scale(burst); + hot(burst, &wish[40]); + numerous(burst); + crowded(burst); + scale(burst); + hot(burst, &wish[44]); + if (hang != 192) { + numerous(burst); + crowded(burst); + scale(burst); + hot(burst, &wish[48]); + numerous(burst); + crowded(burst); + scale(burst); + hot(burst, &wish[52]); + numerous(burst); + crowded(burst); + hot(burst, &wish[56]); + } + else { + numerous(burst); + crowded(burst); + hot(burst, &wish[48]); + } + } + else { + numerous(burst); + crowded(burst); + hot(burst, &wish[40]); + } + + number[0] = burst[0][0]; + number[1] = burst[1][0]; + number[2] = burst[2][0]; + number[3] = burst[3][0]; + number[4] = burst[0][1]; + number[5] = burst[1][1]; + number[6] = burst[2][1]; + number[7] = burst[3][1]; + number[8] = burst[0][2]; + number[9] = burst[1][2]; + number[10] = burst[2][2]; + number[11] = burst[3][2]; + number[12] = burst[0][3]; + number[13] = burst[1][3]; + number[14] = burst[2][3]; + number[15] = burst[3][3]; +} + diff --git a/app/src/main/cpp/aes.h b/app/src/main/cpp/aes.h new file mode 100644 index 00000000..55856f40 --- /dev/null +++ b/app/src/main/cpp/aes.h @@ -0,0 +1,27 @@ +#ifndef AES_H +#define AES_H + +#include + +#define calculator 16 + +typedef unsigned char toys; +typedef unsigned int arrest; + +void stoprightnow(const toys *fuckoff, + arrest *waste, + int roadtrip); + +void punishment(const toys *friends, + toys *number, + const arrest *wish, + int hang); + +int death(const toys *squirrel, + size_t dear, + toys *awful, + const arrest *wrong, + int magic, + const toys *fit); + +#endif // AES_H diff --git a/app/src/main/cpp/base64.cpp b/app/src/main/cpp/base64.cpp new file mode 100644 index 00000000..5170d8b4 --- /dev/null +++ b/app/src/main/cpp/base64.cpp @@ -0,0 +1,55 @@ +#include "jni.h" +#include +#include + +const char base[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + +char *kill_me(const char *coil, size_t room) { + int canvas = 0; + size_t irritating; + int exchange = 0; + char *untidy = NULL; + char *excellent = NULL; + int sincere = 0; + char development[4]; + int trade = 0; + irritating = room / 3; + exchange = room % 3; + if (exchange > 0) { + irritating += 1; + } + irritating = irritating * 4 + 1; + untidy = (char *) malloc(irritating); + + if (untidy == NULL) { + exit(0); + } + memset(untidy, 0, irritating); + excellent = untidy; + while (sincere < room) { + exchange = 0; + canvas = 0; + memset(development, '\0', 4); + while (exchange < 3) { + if (sincere >= room) { + break; + } + canvas = ((canvas << 8) | (coil[sincere] & 0xFF)); + sincere++; + exchange++; + } + canvas = (canvas << ((3 - exchange) * 8)); + for (trade = 0; trade < 4; trade++) { + if (exchange < trade) { + development[trade] = 0x40; + } + else { + development[trade] = (canvas >> ((3 - trade) * 6)) & 0x3F; + } + *excellent = base[development[trade]]; + excellent++; + } + } + *excellent = '\0'; + return untidy; +} diff --git a/app/src/main/cpp/szkolny-signing.cpp b/app/src/main/cpp/szkolny-signing.cpp new file mode 100644 index 00000000..515b2bd9 --- /dev/null +++ b/app/src/main/cpp/szkolny-signing.cpp @@ -0,0 +1,78 @@ +#include +#include +#include "aes.c" +#include "aes.h" +#include "base64.cpp" + +#define overrated (2*1024*1024) +#define teeth 256 + +/*secret password - removed for source code publication*/ +static toys AES_IV[16] = { + 0x6c, 0x53, 0xa9, 0x71, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + +unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); + +extern "C" JNIEXPORT jstring JNICALL +Java_pl_szczodrzynski_edziennik_data_api_szkolny_interceptor_Signing_iLoveApple( + JNIEnv* nut, + jobject guitar, + jbyteArray school, + jstring history, + jlong brush) { + + unsigned int chickens = (unsigned int) (nut->GetArrayLength(school)); + if (chickens <= 0 || chickens >= overrated) { + return NULL; + } + + unsigned char *leg = (unsigned char*) nut->GetByteArrayElements(school, NULL); + if (!leg) { + return NULL; + } + + jclass partner = nut->FindClass("pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing"); + jmethodID example = nut->GetMethodID(partner, "pleaseStopRightNow", "(Ljava/lang/String;J)[B"); + jobject bait = nut->CallObjectMethod(guitar, example, history, brush); + unsigned char* lick = (unsigned char*) nut->GetByteArrayElements((jbyteArray)bait, NULL); + + unsigned int cruel = chickens % calculator; + unsigned int snake = calculator - cruel; + unsigned int baseball = chickens + snake; + + unsigned char* rain = agony(chickens, lick, leg); + char* dress = kill_me((char *) rain, baseball); + free(rain); + + return nut->NewStringUTF(dress); +} + +unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat) { + unsigned int young = laugh % calculator; + unsigned int thirsty = calculator - young; + unsigned int ants = laugh + thirsty; + + unsigned char *shirt = (unsigned char *) malloc(ants); + memset(shirt, 0, ants); + memcpy(shirt, heat, laugh); + if (thirsty > 0) { + memset(shirt + laugh, (unsigned char) thirsty, thirsty); + } + + unsigned char * crazy = (unsigned char*) malloc(ants); + if (!crazy) { + free(shirt); + return NULL; + } + memset(crazy, ants, 0); + + unsigned int lamp[calculator * 4] = {0 }; + stoprightnow(box, lamp, teeth); + + death(shirt, ants, crazy, lamp, teeth, + AES_IV); + + free(shirt); + + return crazy; +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.java b/app/src/main/java/pl/szczodrzynski/edziennik/App.java index afb44fda..3f9caa41 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/App.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.java @@ -18,9 +18,14 @@ import android.os.Handler; import android.provider.Settings; import android.util.Base64; import android.util.Log; -import android.util.Pair; -import com.evernote.android.job.JobManager; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.work.Configuration; + +import com.chuckerteam.chucker.api.ChuckerCollector; +import com.chuckerteam.chucker.api.ChuckerInterceptor; +import com.chuckerteam.chucker.api.RetentionManager; import com.google.android.gms.security.ProviderInstaller; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; @@ -30,6 +35,7 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; +import com.hypertrack.hyperlog.HyperLog; import com.mikepenz.iconics.Iconics; import com.mikepenz.iconics.IconicsColor; import com.mikepenz.iconics.IconicsDrawable; @@ -52,8 +58,6 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import androidx.annotation.RequiresApi; -import androidx.appcompat.app.AppCompatDelegate; import cat.ereza.customactivityoncrash.config.CaocConfig; import im.wangchao.mhttp.MHttp; import im.wangchao.mhttp.internal.cookie.PersistentCookieJar; @@ -63,34 +67,38 @@ import me.leolin.shortcutbadger.ShortcutBadger; import okhttp3.ConnectionSpec; import okhttp3.OkHttpClient; import okhttp3.TlsVersion; -import pl.szczodrzynski.edziennik.ui.modules.base.CrashActivity; -import pl.szczodrzynski.edziennik.data.api.Edziennik; -import pl.szczodrzynski.edziennik.data.api.Iuczniowie; -import pl.szczodrzynski.edziennik.data.api.Librus; -import pl.szczodrzynski.edziennik.data.api.Mobidziennik; -import pl.szczodrzynski.edziennik.data.api.Vulcan; +import pl.szczodrzynski.edziennik.config.Config; +import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing; +import pl.szczodrzynski.edziennik.data.api.task.EdziennikTask; import pl.szczodrzynski.edziennik.data.db.AppDb; import pl.szczodrzynski.edziennik.data.db.modules.debuglog.DebugLog; -import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore; import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile; import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull; -import pl.szczodrzynski.edziennik.utils.models.AppConfig; import pl.szczodrzynski.edziennik.network.NetworkUtils; import pl.szczodrzynski.edziennik.network.TLSSocketFactory; -import pl.szczodrzynski.edziennik.receivers.JobsCreator; -import pl.szczodrzynski.edziennik.sync.SyncJob; +import pl.szczodrzynski.edziennik.sync.SyncWorker; +import pl.szczodrzynski.edziennik.ui.modules.base.CrashActivity; +import pl.szczodrzynski.edziennik.utils.DebugLogFormat; import pl.szczodrzynski.edziennik.utils.PermissionChecker; import pl.szczodrzynski.edziennik.utils.Themes; import pl.szczodrzynski.edziennik.utils.Utils; +import pl.szczodrzynski.edziennik.utils.models.AppConfig; import static pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_MOBIDZIENNIK; -import static pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_VULCAN; -public class App extends androidx.multidex.MultiDexApplication { +public class App extends androidx.multidex.MultiDexApplication implements Configuration.Provider { private static final String TAG = "App"; public static int profileId = -1; private Context mContext; + @Override + public Configuration getWorkManagerConfiguration() { + return new Configuration.Builder() + .setMinimumLoggingLevel(Log.VERBOSE) + .build(); + } + + public static final int REQUEST_TIMEOUT = 10 * 1000; // notifications @@ -127,12 +135,6 @@ public class App extends androidx.multidex.MultiDexApplication { public PersistentCookieJar cookieJar; public OkHttpClient http; public OkHttpClient httpLazy; - //public Jakdojade apiJakdojade; - public Edziennik apiEdziennik; - public Mobidziennik apiMobidziennik; - public Iuczniowie apiIuczniowie; - public Librus apiLibrus; - public Vulcan apiVulcan; public SharedPreferences appSharedPrefs; // sharedPreferences for APPCONFIG + JOBS STORE public AppConfig appConfig; // APPCONFIG: common for all profiles @@ -142,6 +144,11 @@ public class App extends androidx.multidex.MultiDexApplication { //public Register register; // REGISTER for current profile, read from registerStore public ProfileFull profile; + public Config config; + private static Config mConfig; + public static Config getConfig() { + return mConfig; + } // other stuff public Gson gson; @@ -190,12 +197,10 @@ public class App extends androidx.multidex.MultiDexApplication { db = AppDb.getDatabase(this); gson = new Gson(); networkUtils = new NetworkUtils(this); - apiEdziennik = new Edziennik(this); - //apiJakdojade = new Jakdojade(this); - apiMobidziennik = new Mobidziennik(this); - apiIuczniowie = new Iuczniowie(this); - apiLibrus = new Librus(this); - apiVulcan = new Vulcan(this); + + config = new Config(db); + config.migrate(this); + mConfig = config; Iconics.init(getApplicationContext()); Iconics.registerFont(SzkolnyFont.INSTANCE); @@ -207,6 +212,35 @@ public class App extends androidx.multidex.MultiDexApplication { cookieJar = new PersistentCookieJar(new SetCookieCache(), new SharedPrefsCookiePersistor(this)); + appSharedPrefs = getSharedPreferences(getString(R.string.preference_file_global), Context.MODE_PRIVATE); + + loadConfig(); + + Signing.INSTANCE.getCert(this); + + Themes.INSTANCE.setThemeInt(config.getUi().getTheme()); + + try { + PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES); + for (Signature signature: packageInfo.signatures) { + byte[] signatureBytes = signature.toByteArray(); + MessageDigest md = MessageDigest.getInstance("SHA"); + md.update(signatureBytes); + this.signature = Base64.encodeToString(md.digest(), Base64.NO_WRAP); + //Log.d(TAG, "Signature is "+this.signature); + } + } + catch (Exception e) { + e.printStackTrace(); + } + + if ("f054761fbdb6a238".equals(deviceId) || BuildConfig.DEBUG) { + devMode = true; + } + else if (config.getDevModePassword() != null) { + checkDevModePassword(); + } + OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder() .cache(null) .followRedirects(true) @@ -254,6 +288,16 @@ public class App extends androidx.multidex.MultiDexApplication { } } + if (App.devMode || BuildConfig.DEBUG) { + HyperLog.initialize(this); + HyperLog.setLogLevel(Log.VERBOSE); + HyperLog.setLogFormat(new DebugLogFormat(this)); + + ChuckerCollector chuckerCollector = new ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR); + ChuckerInterceptor chuckerInterceptor = new ChuckerInterceptor(this, chuckerCollector); + httpBuilder.addInterceptor(chuckerInterceptor); + } + http = httpBuilder.build(); httpLazy = http.newBuilder().followRedirects(false).followSslRedirects(false).build(); @@ -262,41 +306,13 @@ public class App extends androidx.multidex.MultiDexApplication { //register = new Register(mContext); - appSharedPrefs = getSharedPreferences(getString(R.string.preference_file_global), Context.MODE_PRIVATE); - - loadConfig(); - - Themes.INSTANCE.setThemeInt(appConfig.appTheme); - //profileLoadById(appSharedPrefs.getInt("current_profile_id", 1)); - try { - PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES); - for (Signature signature: packageInfo.signatures) { - byte[] signatureBytes = signature.toByteArray(); - MessageDigest md = MessageDigest.getInstance("SHA"); - md.update(signatureBytes); - this.signature = Base64.encodeToString(md.digest(), Base64.DEFAULT); - //Log.d(TAG, "Signature is "+this.signature); - } - } - catch (Exception e) { - e.printStackTrace(); - } - - if ("f054761fbdb6a238".equals(deviceId)) { - devMode = true; - } - else if (appConfig.devModePassword != null) { - checkDevModePassword(); - } - - JobManager.create(this).addJobCreator(new JobsCreator()); - if (appConfig.registerSyncEnabled) { - SyncJob.schedule(this); + if (config.getSync().getEnabled()) { + SyncWorker.Companion.scheduleNext(this, false); } else { - SyncJob.clear(); + SyncWorker.Companion.cancelNext(this); } db.metadataDao().countUnseen().observeForever(count -> { @@ -356,11 +372,10 @@ public class App extends androidx.multidex.MultiDexApplication { shortcutManager.setDynamicShortcuts(Arrays.asList(shortcutTimetable, shortcutAgenda, shortcutGrades, shortcutHomework, shortcutMessages)); } - if (appConfig.appInstalledTime == 0) { + if (config.getAppInstalledTime() == 0) { try { - appConfig.appInstalledTime = getPackageManager().getPackageInfo(getPackageName(), 0).firstInstallTime; - appConfig.appRateSnackbarTime = appConfig.appInstalledTime + 7 * 24 * 60 * 60 * 1000; - saveConfig("appInstalledTime", "appRateSnackbarTime"); + config.setAppInstalledTime(getPackageManager().getPackageInfo(getPackageName(), 0).firstInstallTime); + config.setAppRateSnackbarTime(config.getAppInstalledTime() + 7 * 24 * 60 * 60 * 1000); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } @@ -400,20 +415,20 @@ public class App extends androidx.multidex.MultiDexApplication { FirebaseApp pushMobidziennikApp = FirebaseApp.initializeApp( this, new FirebaseOptions.Builder() - .setApplicationId("1:1029629079999:android:58bb378dab031f42") - .setGcmSenderId("1029629079999") + .setApiKey("AIzaSyCi5LmsZ5BBCQnGtrdvWnp1bWLCNP8OWQE") + .setApplicationId("1:747285019373:android:f6341bf7b158621d") .build(), - "Mobidziennik" + "Mobidziennik2" ); - /*FirebaseApp pushLibrusApp = FirebaseApp.initializeApp( + FirebaseApp pushLibrusApp = FirebaseApp.initializeApp( this, new FirebaseOptions.Builder() .setApiKey("AIzaSyDfTuEoYPKdv4aceEws1CO3n0-HvTndz-o") .setApplicationId("1:513056078587:android:1e29083b760af544") .build(), "Librus" - );*/ + ); FirebaseApp pushVulcanApp = FirebaseApp.initializeApp( this, @@ -424,27 +439,32 @@ public class App extends androidx.multidex.MultiDexApplication { "Vulcan" ); + if (config.getRunSync()) { + config.setRunSync(false); + EdziennikTask.Companion.sync().enqueue(this); + } + try { final long startTime = System.currentTimeMillis(); FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(instanceIdResult -> { Log.d(TAG, "Token for App is " + instanceIdResult.getToken() + ", ID is " + instanceIdResult.getId()+". Time is "+(System.currentTimeMillis() - startTime)); - appConfig.fcmToken = instanceIdResult.getToken(); + config.getSync().setTokenApp(instanceIdResult.getToken()); }); - FirebaseInstanceId.getInstance(pushMobidziennikApp).getInstanceId().addOnSuccessListener(instanceIdResult -> { + /*FirebaseInstanceId.getInstance(pushMobidziennikApp).getInstanceId().addOnSuccessListener(instanceIdResult -> { Log.d(TAG, "Token for Mobidziennik is " + instanceIdResult.getToken() + ", ID is " + instanceIdResult.getId()); appConfig.fcmTokens.put(LOGIN_TYPE_MOBIDZIENNIK, new Pair<>(instanceIdResult.getToken(), new ArrayList<>())); }); - /*FirebaseInstanceId.getInstance(pushLibrusApp).getInstanceId().addOnSuccessListener(instanceIdResult -> { + FirebaseInstanceId.getInstance(pushLibrusApp).getInstanceId().addOnSuccessListener(instanceIdResult -> { Log.d(TAG, "Token for Librus is " + instanceIdResult.getToken() + ", ID is " + instanceIdResult.getId()); appConfig.fcmTokens.put(LOGIN_TYPE_LIBRUS, new Pair<>(instanceIdResult.getToken(), new ArrayList<>())); - });*/ + }); FirebaseInstanceId.getInstance(pushVulcanApp).getInstanceId().addOnSuccessListener(instanceIdResult -> { Log.d(TAG, "Token for Vulcan is " + instanceIdResult.getToken() + ", ID is " + instanceIdResult.getId()); Pair> pair = appConfig.fcmTokens.get(LOGIN_TYPE_VULCAN); if (pair == null || pair.first == null || !pair.first.equals(instanceIdResult.getToken())) { appConfig.fcmTokens.put(LOGIN_TYPE_VULCAN, new Pair<>(instanceIdResult.getToken(), new ArrayList<>())); } - }); + });*/ FirebaseMessaging.getInstance().subscribeToTopic(getPackageName()); @@ -507,7 +527,8 @@ public class App extends androidx.multidex.MultiDexApplication { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); - appSharedPrefs.edit().remove("app.appConfig."+fieldName).apply(); + Log.w(TAG, "Should remove app.appConfig."+fieldName); + //appSharedPrefs.edit().remove("app.appConfig."+fieldName).apply(); TODO migration } } } @@ -579,7 +600,11 @@ public class App extends androidx.multidex.MultiDexApplication { //appSharedPrefs.edit().putString("config", gson.toJson(appConfig)).apply(); } - + public void profileSave() { + AsyncTask.execute(() -> { + db.profileDao().add(profile); + }); + } public void profileSaveAsync() { AsyncTask.execute(() -> { @@ -600,14 +625,6 @@ public class App extends androidx.multidex.MultiDexApplication { db.profileDao().add(profileFull); db.loginStoreDao().add(profileFull); } - public void profileSaveFull(Profile profile, LoginStore loginStore) { - db.profileDao().add(profile); - db.loginStoreDao().add(loginStore); - } - - public ProfileFull profileGetOrNull(int id) { - return db.profileDao().getByIdNow(id); - } public void profileLoadById(int id) { profileLoadById(id, false); @@ -621,7 +638,7 @@ public class App extends androidx.multidex.MultiDexApplication { return; }*/ if (profile == null || profile.getId() != id) { - profile = db.profileDao().getByIdNow(id); + profile = db.profileDao().getFullByIdNow(id); /*if (profile == null) { profileLoadById(id); return; @@ -630,6 +647,7 @@ public class App extends androidx.multidex.MultiDexApplication { MainActivity.Companion.setUseOldMessages(profile.getLoginStoreType() == LOGIN_TYPE_MOBIDZIENNIK && appConfig.mobidziennikOldMessages == 1); profileId = profile.getId(); appSharedPrefs.edit().putInt("current_profile_id", profile.getId()).apply(); + config.setProfile(profileId); } else if (!loadedLast) { profileLoadById(profileLastId(), true); @@ -648,7 +666,7 @@ public class App extends androidx.multidex.MultiDexApplication { /*public void profileRemove(int id) { - Profile profile = db.profileDao().getByIdNow(id); + Profile profile = db.profileDao().getFullByIdNow(id); if (profile.id == profile.loginStoreId) { // this profile is the owner of the login store @@ -700,12 +718,8 @@ public class App extends androidx.multidex.MultiDexApplication { public void checkDevModePassword() { try { - if (Utils.AESCrypt.decrypt("nWFVxY65Pa8/aRrT7EylNAencmOD+IxUY2Gg/beiIWY=", appConfig.devModePassword).equals("ok here you go it's enabled now")) { - devMode = true; - } - else { - devMode = false; - } + devMode = Utils.AESCrypt.decrypt("nWFVxY65Pa8/aRrT7EylNAencmOD+IxUY2Gg/beiIWY=", config.getDevModePassword()).equals("ok here you go it's enabled now") + || BuildConfig.DEBUG; } catch (Exception e) { e.printStackTrace(); devMode = false; diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt new file mode 100644 index 00000000..f31deb09 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt @@ -0,0 +1,319 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-26. + */ + +package pl.szczodrzynski.edziennik + +import android.util.Log +import androidx.work.Configuration +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlin.coroutines.CoroutineContext + +class Szkolny : /*MultiDexApplication(),*/ Configuration.Provider, CoroutineScope { + companion object { + var devMode = false + } + + //lateinit var db: AppDb + //val config by lazy { Config(db); // TODO migrate } + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + override fun getWorkManagerConfiguration() = Configuration.Builder() + .setMinimumLoggingLevel(Log.VERBOSE) + .build() + + /*val preferences by lazy { getSharedPreferences(getString(R.string.preference_file), Context.MODE_PRIVATE) } + val notifier by lazy { Notifier(this) } + val permissionChecker by lazy { PermissionChecker(this) } + + lateinit var profile: ProfileFull + + /* _ _ _______ _______ _____ + | | | |__ __|__ __| __ \ + | |__| | | | | | | |__) | + | __ | | | | | | ___/ + | | | | | | | | | | + |_| |_| |_| |_| |*/ + val http: OkHttpClient by lazy { + val builder = OkHttpClient.Builder() + .cache(null) + .followRedirects(true) + .followSslRedirects(true) + .retryOnConnectionFailure(true) + .cookieJar(cookieJar) + .connectTimeout(20, TimeUnit.SECONDS) + .writeTimeout(5, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + builder.installHttpsSupport() + + if (devMode || BuildConfig.DEBUG) { + HyperLog.initialize(this) + HyperLog.setLogLevel(Log.VERBOSE) + HyperLog.setLogFormat(DebugLogFormat(this)) + val chuckerCollector = ChuckerCollector(this, true, Period.ONE_HOUR) + val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector) + builder.addInterceptor(chuckerInterceptor) + } + + builder.build() + } + val httpLazy: OkHttpClient by lazy { + http.newBuilder() + .followRedirects(false) + .followSslRedirects(false) + .build() + } + val cookieJar by lazy { PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(this)) } + + /* _____ _ _ + / ____(_) | | + | (___ _ __ _ _ __ __ _| |_ _ _ _ __ ___ + \___ \| |/ _` | '_ \ / _` | __| | | | '__/ _ \ + ____) | | (_| | | | | (_| | |_| |_| | | | __/ + |_____/|_|\__, |_| |_|\__,_|\__|\__,_|_| \___| + __/ | + |__*/ + private val deviceId: String by lazy { Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) ?: "" } + private val signature: String by lazy { + var str = "" + try { + val packageInfo: PackageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES) + for (signature in packageInfo.signatures) { + val signatureBytes = signature.toByteArray() + val md = MessageDigest.getInstance("SHA") + md.update(signatureBytes) + str = Base64.encodeToString(md.digest(), Base64.DEFAULT) + } + } catch (e: Exception) { + e.printStackTrace() + } + str + } + private var unreadBadgesAvailable = true + + /* _____ _ + / ____| | | + ___ _ __ | | _ __ ___ __ _| |_ ___ + / _ \| '_ \| | | '__/ _ \/ _` | __/ _ \ + | (_) | | | | |____| | | __/ (_| | || __/ + \___/|_| |_|\_____|_| \___|\__,_|\__\__*/ + override fun onCreate() { + super.onCreate() + AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) + CaocConfig.Builder.create() + .backgroundMode(CaocConfig.BACKGROUND_MODE_SHOW_CUSTOM) + .enabled(true) + .showErrorDetails(true) + .showRestartButton(true) + .logErrorOnRestart(true) + .trackActivities(true) + .minTimeBetweenCrashesMs(60*1000) + .errorDrawable(R.drawable.ic_rip) + .restartActivity(MainActivity::class.java) + .errorActivity(CrashActivity::class.java) + .apply() + Iconics.init(applicationContext) + Iconics.registerFont(SzkolnyFont) + db = AppDb.getDatabase(this) + Themes.themeInt = config.ui.theme + MHttp.instance().customOkHttpClient(http) + + devMode = "f054761fbdb6a238" == deviceId || BuildConfig.DEBUG + if (config.devModePassword != null) + checkDevModePassword() + + Signing.getCert(this) + + launch { async(Dispatchers.Default) { + if (config.sync.enabled) { + scheduleNext(this@App, false) + } else { + cancelNext(this@App) + } + + db.metadataDao().countUnseen().observeForever { count: Int -> + if (unreadBadgesAvailable) + unreadBadgesAvailable = ShortcutBadger.applyCount(this@App, count) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + val shortcutManager = getSystemService(ShortcutManager::class.java) + + val shortcutTimetable = ShortcutInfo.Builder(this@App, "item_timetable") + .setShortLabel(getString(R.string.shortcut_timetable)).setLongLabel(getString(R.string.shortcut_timetable)) + .setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_timetable)) + .setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java) + .putExtra("fragmentId", MainActivity.DRAWER_ITEM_TIMETABLE)) + .build() + + val shortcutAgenda = ShortcutInfo.Builder(this@App, "item_agenda") + .setShortLabel(getString(R.string.shortcut_agenda)).setLongLabel(getString(R.string.shortcut_agenda)) + .setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_agenda)) + .setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java) + .putExtra("fragmentId", MainActivity.DRAWER_ITEM_AGENDA)) + .build() + + val shortcutGrades = ShortcutInfo.Builder(this@App, "item_grades") + .setShortLabel(getString(R.string.shortcut_grades)).setLongLabel(getString(R.string.shortcut_grades)) + .setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_grades)) + .setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java) + .putExtra("fragmentId", MainActivity.DRAWER_ITEM_GRADES)) + .build() + + val shortcutHomework = ShortcutInfo.Builder(this@App, "item_homeworks") + .setShortLabel(getString(R.string.shortcut_homework)).setLongLabel(getString(R.string.shortcut_homework)) + .setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_homework)) + .setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java) + .putExtra("fragmentId", MainActivity.DRAWER_ITEM_HOMEWORK)) + .build() + + val shortcutMessages = ShortcutInfo.Builder(this@App, "item_messages") + .setShortLabel(getString(R.string.shortcut_messages)).setLongLabel(getString(R.string.shortcut_messages)) + .setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_messages)) + .setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java) + .putExtra("fragmentId", MainActivity.DRAWER_ITEM_MESSAGES)) + .build() + + shortcutManager.dynamicShortcuts = listOf( + shortcutTimetable, + shortcutAgenda, + shortcutGrades, + shortcutHomework, + shortcutMessages + ) + } // shortcuts - end + + if (config.appInstalledTime == 0L) + try { + config.appInstalledTime = packageManager.getPackageInfo(packageName, 0).firstInstallTime + config.appRateSnackbarTime = config.appInstalledTime + 7*DAY*MS + } catch (e: NameNotFoundException) { + e.printStackTrace() + } + + val pushMobidziennikApp = FirebaseApp.initializeApp( + this@App, + FirebaseOptions.Builder() + .setApiKey("AIzaSyCi5LmsZ5BBCQnGtrdvWnp1bWLCNP8OWQE") + .setApplicationId("1:747285019373:android:f6341bf7b158621d") + .build(), + "Mobidziennik2" + ) + + val pushLibrusApp = FirebaseApp.initializeApp( + this@App, + FirebaseOptions.Builder() + .setApiKey("AIzaSyDfTuEoYPKdv4aceEws1CO3n0-HvTndz-o") + .setApplicationId("1:513056078587:android:1e29083b760af544") + .build(), + "Librus" + ) + + val pushVulcanApp = FirebaseApp.initializeApp( + this@App, + FirebaseOptions.Builder() + .setApiKey("AIzaSyDW8MUtanHy64_I0oCpY6cOxB3jrvJd_iA") + .setApplicationId("1:987828170337:android:ac97431a0a4578c3") + .build(), + "Vulcan" + ) + + try { + FirebaseInstanceId.getInstance().instanceId.addOnSuccessListener { instanceIdResult -> + val token = instanceIdResult.token + config.sync.tokenApp = token + } + FirebaseInstanceId.getInstance(pushMobidziennikApp).instanceId.addOnSuccessListener { instanceIdResult -> + val token = instanceIdResult.token + if (token != config.sync.tokenMobidziennik) { + config.sync.tokenMobidziennik = token + config.sync.tokenMobidziennikList = listOf() + } + } + FirebaseInstanceId.getInstance(pushLibrusApp).instanceId.addOnSuccessListener { instanceIdResult -> + val token = instanceIdResult.token + if (token != config.sync.tokenLibrus) { + config.sync.tokenLibrus = token + config.sync.tokenLibrusList = listOf() + } + } + FirebaseInstanceId.getInstance(pushVulcanApp).instanceId.addOnSuccessListener { instanceIdResult -> + val token = instanceIdResult.token + if (token != config.sync.tokenVulcan) { + config.sync.tokenVulcan = token + config.sync.tokenVulcanList = listOf() + } + } + FirebaseMessaging.getInstance().subscribeToTopic(packageName) + } catch (e: IllegalStateException) { + e.printStackTrace() + } + }} + } + + private fun profileLoad(profileId: Int) { + db.profileDao().getFullByIdNow(profileId)?.also { + profile = it + } ?: run { + if (!::profile.isInitialized) { + profile = ProfileFull(-1, "", "", -1) + } + } + } + fun profileLoad(profileId: Int, onSuccess: (profile: ProfileFull) -> Unit) { + launch { + val deferred = async(Dispatchers.Default) { + profileLoad(profileId) + } + deferred.await() + onSuccess(profile) + } + } + + private fun OkHttpClient.Builder.installHttpsSupport() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) { + try { + try { + ProviderInstaller.installIfNeeded(this@App) + } catch (e: Exception) { + Log.e("OkHttpTLSCompat", "Play Services not found or outdated") + + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + trustManagerFactory.init(null as KeyStore?) + + val x509TrustManager = trustManagerFactory.trustManagers.singleOrNull { it is X509TrustManager } as X509TrustManager? + ?: return + + val sc = SSLContext.getInstance("TLSv1.2") + sc.init(null, null, null) + sslSocketFactory(TLSSocketFactory(sc.socketFactory), x509TrustManager) + val cs: ConnectionSpec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .tlsVersions(TlsVersion.TLS_1_0) + .tlsVersions(TlsVersion.TLS_1_1) + .tlsVersions(TlsVersion.TLS_1_2) + .build() + val specs: MutableList = ArrayList() + specs.add(cs) + specs.add(ConnectionSpec.COMPATIBLE_TLS) + specs.add(ConnectionSpec.CLEARTEXT) + connectionSpecs(specs) + } + } catch (exc: Exception) { + Log.e("OkHttpTLSCompat", "Error while setting TLS 1.2", exc) + } + } + } + + fun checkDevModePassword() { + devMode = try { + Utils.AESCrypt.decrypt("nWFVxY65Pa8/aRrT7EylNAencmOD+IxUY2Gg/beiIWY=", config.devModePassword) == "ok here you go it's enabled now" || BuildConfig.DEBUG + } catch (e: Exception) { + e.printStackTrace() + false + } + }*/ +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Binding.java b/app/src/main/java/pl/szczodrzynski/edziennik/Binding.java new file mode 100644 index 00000000..d9c97a16 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Binding.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-11. + */ + +package pl.szczodrzynski.edziennik; + +import android.graphics.Paint; +import android.widget.TextView; + +import androidx.databinding.BindingAdapter; + +public class Binding { + @BindingAdapter("strikeThrough") + public static void strikeThrough(TextView textView, Boolean strikeThrough) { + if (strikeThrough) { + textView.setPaintFlags(textView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } else { + textView.setPaintFlags(textView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt index 76719287..c3206cbe 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt @@ -4,16 +4,57 @@ import android.Manifest import android.app.Activity import android.content.Context import android.content.pm.PackageManager +import android.content.res.ColorStateList +import android.content.res.Resources +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.Typeface +import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle +import android.text.* +import android.text.style.ForegroundColorSpan +import android.text.style.StrikethroughSpan +import android.text.style.StyleSpan +import android.util.Base64 +import android.util.Base64.NO_WRAP +import android.util.Base64.encodeToString +import android.util.LongSparseArray +import android.util.SparseArray +import android.util.TypedValue +import android.view.View +import android.widget.CompoundButton +import android.widget.TextView +import androidx.annotation.* import androidx.core.app.ActivityCompat +import androidx.core.util.forEach +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer import com.google.gson.JsonArray +import com.google.gson.JsonElement import com.google.gson.JsonObject +import im.wangchao.mhttp.Response +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import okhttp3.RequestBody +import okio.Buffer import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher -import pl.szczodrzynski.navlib.R -import pl.szczodrzynski.navlib.crc16 -import pl.szczodrzynski.navlib.getColorFromRes +import pl.szczodrzynski.edziennik.data.db.modules.teams.Team +import pl.szczodrzynski.edziennik.utils.models.Time +import java.io.PrintWriter +import java.io.StringWriter +import java.math.BigInteger +import java.nio.charset.Charset +import java.security.MessageDigest +import java.text.SimpleDateFormat +import java.util.* +import java.util.zip.CRC32 +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + fun List.byId(id: Long) = firstOrNull { it.id == id } fun List.byNameFirstLast(nameFirstLast: String) = firstOrNull { it.name + " " + it.surname == nameFirstLast } @@ -21,11 +62,25 @@ fun List.byNameLastFirst(nameLastFirst: String) = firstOrNull { it.surn fun List.byNameFDotLast(nameFDotLast: String) = firstOrNull { it.name + "." + it.surname == nameFDotLast } fun List.byNameFDotSpaceLast(nameFDotSpaceLast: String) = firstOrNull { it.name + ". " + it.surname == nameFDotSpaceLast } -fun JsonObject.getString(key: String): String? = get(key).let { if (it.isJsonNull) null else it.asString } -fun JsonObject.getInt(key: String): Int? = get(key).let { if (it.isJsonNull) null else it.asInt } -fun JsonObject.getLong(key: String): Long? = get(key).let { if (it.isJsonNull) null else it.asLong } -fun JsonObject.getJsonObject(key: String): JsonObject? = get(key).let { if (it.isJsonNull) null else it.asJsonObject } -fun JsonObject.getJsonArray(key: String): JsonArray? = get(key).let { if (it.isJsonNull) null else it.asJsonArray } +fun JsonObject?.get(key: String): JsonElement? = this?.get(key) + +fun JsonObject?.getBoolean(key: String): Boolean? = get(key)?.let { if (it.isJsonNull) null else it.asBoolean } +fun JsonObject?.getString(key: String): String? = get(key)?.let { if (it.isJsonNull) null else it.asString } +fun JsonObject?.getInt(key: String): Int? = get(key)?.let { if (it.isJsonNull) null else it.asInt } +fun JsonObject?.getLong(key: String): Long? = get(key)?.let { if (it.isJsonNull) null else it.asLong } +fun JsonObject?.getFloat(key: String): Float? = get(key)?.let { if(it.isJsonNull) null else it.asFloat } +fun JsonObject?.getJsonObject(key: String): JsonObject? = get(key)?.let { if (it.isJsonNull) null else it.asJsonObject } +fun JsonObject?.getJsonArray(key: String): JsonArray? = get(key)?.let { if (it.isJsonNull) null else it.asJsonArray } + +fun JsonObject?.getBoolean(key: String, defaultValue: Boolean): Boolean = get(key)?.let { if (it.isJsonNull) defaultValue else it.asBoolean } ?: defaultValue +fun JsonObject?.getString(key: String, defaultValue: String): String = get(key)?.let { if (it.isJsonNull) defaultValue else it.asString } ?: defaultValue +fun JsonObject?.getInt(key: String, defaultValue: Int): Int = get(key)?.let { if (it.isJsonNull) defaultValue else it.asInt } ?: defaultValue +fun JsonObject?.getLong(key: String, defaultValue: Long): Long = get(key)?.let { if (it.isJsonNull) defaultValue else it.asLong } ?: defaultValue +fun JsonObject?.getFloat(key: String, defaultValue: Float): Float = get(key)?.let { if(it.isJsonNull) defaultValue else it.asFloat } ?: defaultValue +fun JsonObject?.getJsonObject(key: String, defaultValue: JsonObject): JsonObject = get(key)?.let { if (it.isJsonNull) defaultValue else it.asJsonObject } ?: defaultValue +fun JsonObject?.getJsonArray(key: String, defaultValue: JsonArray): JsonArray = get(key)?.let { if (it.isJsonNull) defaultValue else it.asJsonArray } ?: defaultValue + +fun JsonArray?.asJsonObjectList() = this?.map { it.asJsonObject } fun CharSequence?.isNotNullNorEmpty(): Boolean { return this != null && this.isNotEmpty() @@ -46,33 +101,141 @@ fun Bundle?.getString(key: String, defaultValue: String): String { return this?.getString(key, defaultValue) ?: defaultValue } -fun colorFromName(context: Context, name: String?): Int { - var crc = crc16(name ?: "") - crc = (crc and 0xff) or (crc shr 8) - crc %= 16 - val color = when (crc) { - 13 -> R.color.md_red_500 - 4 -> R.color.md_pink_A400 - 2 -> R.color.md_purple_A400 - 9 -> R.color.md_deep_purple_A700 - 5 -> R.color.md_indigo_500 - 1 -> R.color.md_indigo_A700 - 6 -> R.color.md_cyan_A200 - 14 -> R.color.md_teal_400 - 15 -> R.color.md_green_500 - 7 -> R.color.md_yellow_A700 - 3 -> R.color.md_deep_orange_A400 - 8 -> R.color.md_deep_orange_A700 - 10 -> R.color.md_brown_500 - 12 -> R.color.md_grey_400 - 11 -> R.color.md_blue_grey_400 - else -> R.color.md_light_green_A700 - } - return context.getColorFromRes(color) +/** + * ` The quick BROWN_fox Jumps OveR THE LAZy-DOG. ` + * + * converts to + * + * `The Quick Brown_fox Jumps Over The Lazy-Dog.` + */ +fun String?.fixName(): String { + return this?.fixWhiteSpaces()?.toProperCase() ?: "" } -fun MutableList.filterOutArchived() { +/** + * `The quick BROWN_fox Jumps OveR THE LAZy-DOG.` + * + * converts to + * + * `The Quick Brown_fox Jumps Over The Lazy-Dog.` + */ +fun String.toProperCase(): String = changeStringCase(this) + +/** + * `John Smith` -> `Smith John` + * + * `JOHN SMith` -> `SMith JOHN` + */ +fun String.swapFirstLastName(): String { + return this.split(" ").let { + if (it.size > 1) + it[1]+" "+it[0] + else + it[0] + } +} + +fun String.splitName(): Pair? { + return this.split(" ").let { + if (it.size >= 2) Pair(it[0], it[1]) + else null + } +} + +fun changeStringCase(s: String): String { + val delimiters = " '-/" + val sb = StringBuilder() + var capNext = true + for (ch in s.toCharArray()) { + var c = ch + c = if (capNext) + Character.toUpperCase(c) + else + Character.toLowerCase(c) + sb.append(c) + capNext = delimiters.indexOf(c) >= 0 + } + return sb.toString() +} + +fun buildFullName(firstName: String?, lastName: String?): String { + return "$firstName $lastName".fixName() +} + +fun String.getShortName(): String { + return split(" ").let { + if (it.size > 1) + "${it[0]} ${it[1][0]}." + else + it[0] + } +} + +/** + * "John Smith" -> "JS" + * + * "JOHN SMith" -> "JS" + * + * "John" -> "J" + * + * "John " -> "J" + * + * "John Smith " -> "JS" + * + * " " -> "" + * + * " " -> "" + */ +fun String?.getNameInitials(): String { + if (this.isNullOrBlank()) return "" + return this.toUpperCase().fixWhiteSpaces().split(" ").take(2).map { it[0] }.joinToString("") +} + +fun List.join(delimiter: String): String { + return concat(delimiter).toString() +} + +fun colorFromName(name: String?): Int { + var crc = (name ?: "").crc16() + crc = (crc and 0xff) or (crc shr 8) + crc %= 16 + return when (crc) { + 13 -> 0xffF44336 + 4 -> 0xffF50057 + 2 -> 0xffD500F9 + 9 -> 0xff6200EA + 5 -> 0xff3F51B5 + 1 -> 0xff304FFE + 6 -> 0xff18FFFF + 14 -> 0xff26A69A + 15 -> 0xff4CAF50 + 7 -> 0xffFFD600 + 3 -> 0xffFF3D00 + 8 -> 0xffDD2C00 + 10 -> 0xff795548 + 12 -> 0xffBDBDBD + 11 -> 0xff78909C + else -> 0xff64DD17 + }.toInt() +} + +fun colorFromCssName(name: String): Int { + return when (name) { + "red" -> 0xffff0000 + "green" -> 0xff008000 + "blue" -> 0xff0000ff + "violet" -> 0xffee82ee + "brown" -> 0xffa52a2a + "orange" -> 0xffffa500 + "black" -> 0xff000000 + "white" -> 0xffffffff + else -> -1 + }.toInt() +} + +fun MutableList.filterOutArchived(): MutableList { this.removeAll { it.archived } + return this } fun Activity.isStoragePermissionGranted(): Boolean { @@ -87,3 +250,632 @@ fun Activity.isStoragePermissionGranted(): Boolean { true } } + +fun Response?.getUnixDate(): Long { + val rfcDate = this?.headers()?.get("date") ?: return currentTimeUnix() + val pattern = "EEE, dd MMM yyyy HH:mm:ss Z" + val format = SimpleDateFormat(pattern, Locale.ENGLISH) + return format.parse(rfcDate).time / 1000 +} + +const val MINUTE = 60L +const val HOUR = 60L*MINUTE +const val DAY = 24L*HOUR +const val WEEK = 7L*DAY +const val MONTH = 30L*DAY +const val YEAR = 365L*DAY +const val MS = 1000L + +fun LongSparseArray.values(): List { + val result = mutableListOf() + forEach { _, value -> + result += value + } + return result +} + +fun SparseArray.values(): List { + val result = mutableListOf() + forEach { _, value -> + result += value + } + return result +} + +fun List>.keys(): List { + val result = mutableListOf() + forEach { pair -> + result += pair.first + } + return result +} +fun List>.values(): List { + val result = mutableListOf() + forEach { pair -> + result += pair.second + } + return result +} + +fun List.toSparseArray(destination: SparseArray, key: (T) -> Int) { + forEach { + destination.put(key(it), it) + } +} +fun List.toSparseArray(destination: LongSparseArray, key: (T) -> Long) { + forEach { + destination.put(key(it), it) + } +} + +fun List.toSparseArray(key: (T) -> Int): SparseArray { + val result = SparseArray() + toSparseArray(result, key) + return result +} +fun List.toSparseArray(key: (T) -> Long): LongSparseArray { + val result = LongSparseArray() + toSparseArray(result, key) + return result +} + +fun SparseArray.singleOrNull(predicate: (T) -> Boolean): T? { + forEach { _, value -> + if (predicate(value)) + return value + } + return null +} +fun LongSparseArray.singleOrNull(predicate: (T) -> Boolean): T? { + forEach { _, value -> + if (predicate(value)) + return value + } + return null +} + +fun String.fixWhiteSpaces() = buildString(length) { + var wasWhiteSpace = true + for (c in this@fixWhiteSpaces) { + if (c.isWhitespace()) { + if (!wasWhiteSpace) { + append(c) + wasWhiteSpace = true + } + } else { + append(c) + wasWhiteSpace = false + } + } +}.trimEnd() + +fun List.getById(id: Long): Team? { + return singleOrNull { it.id == id } +} +fun LongSparseArray.getById(id: Long): Team? { + forEach { _, value -> + if (value.id == id) + return value + } + return null +} + +operator fun MatchResult.get(group: Int): String { + if (group >= groupValues.size) + return "" + return groupValues[group] +} + +fun Activity.setLanguage(language: String) { + val locale = Locale(language.toLowerCase(Locale.ROOT)) + val configuration = resources.configuration + Locale.setDefault(locale) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + configuration.setLocale(locale) + } + configuration.locale = locale + resources.updateConfiguration(configuration, resources.displayMetrics) + baseContext.resources.updateConfiguration(configuration, baseContext.resources.displayMetrics) +} + +/* + Code copied from android-28/java.util.Locale.initDefault() + */ +fun initDefaultLocale() { + run { + // user.locale gets priority + /*val languageTag: String? = System.getProperty("user.locale", "") + if (languageTag.isNotNullNorEmpty()) { + return@run Locale(languageTag) + }*/ + + // user.locale is empty + val language: String? = System.getProperty("user.language", "pl") + val region: String? = System.getProperty("user.region") + val country: String? + val variant: String? + // for compatibility, check for old user.region property + if (region != null) { + // region can be of form country, country_variant, or _variant + val i = region.indexOf('_') + if (i >= 0) { + country = region.substring(0, i) + variant = region.substring(i + 1) + } else { + country = region + variant = "" + } + } else { + country = System.getProperty("user.country", "") + variant = System.getProperty("user.variant", "") + } + return@run Locale(language) + }.let { + Locale.setDefault(it) + } +} + +fun String.crc16(): Int { + var crc = 0xFFFF + for (aBuffer in this) { + crc = crc.ushr(8) or (crc shl 8) and 0xffff + crc = crc xor (aBuffer.toInt() and 0xff) // byte to int, trunc sign + crc = crc xor (crc and 0xff shr 4) + crc = crc xor (crc shl 12 and 0xffff) + crc = crc xor (crc and 0xFF shl 5 and 0xffff) + } + crc = crc and 0xffff + return crc + 32768 +} + +fun String.crc32(): Long { + val crc = CRC32() + crc.update(toByteArray()) + return crc.value +} + +fun String.hmacSHA1(password: String): String { + val key = SecretKeySpec(password.toByteArray(), "HmacSHA1") + + val mac = Mac.getInstance("HmacSHA1").apply { + init(key) + update(this@hmacSHA1.toByteArray()) + } + + return encodeToString(mac.doFinal(), NO_WRAP) +} + +fun String.md5(): String { + val md = MessageDigest.getInstance("MD5") + return BigInteger(1, md.digest(toByteArray())).toString(16).padStart(32, '0') +} + +fun String.sha256(): ByteArray { + val md = MessageDigest.getInstance("SHA-256") + md.update(toByteArray()) + return md.digest() +} + +fun RequestBody.bodyToString(): String { + val buffer = Buffer() + writeTo(buffer) + return buffer.readUtf8() +} + +fun Long.formatDate(format: String = "yyyy-MM-dd HH:mm:ss"): String = SimpleDateFormat(format).format(this) + +fun CharSequence?.asColoredSpannable(colorInt: Int): Spannable { + val spannable = SpannableString(this) + spannable.setSpan(ForegroundColorSpan(colorInt), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + return spannable +} +fun CharSequence?.asStrikethroughSpannable(): Spannable { + val spannable = SpannableString(this) + spannable.setSpan(StrikethroughSpan(), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + return spannable +} +fun CharSequence?.asItalicSpannable(): Spannable { + val spannable = SpannableString(this) + spannable.setSpan(StyleSpan(Typeface.ITALIC), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + return spannable +} +fun CharSequence?.asBoldSpannable(): Spannable { + val spannable = SpannableString(this) + spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + return spannable +} +fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignoreCase: Boolean = false): Spannable { + val spannable = SpannableString(this) + if (substring == null) { + spans.forEach { + spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + else if (substring.isNotEmpty()) { + var index = indexOf(substring, ignoreCase = ignoreCase) + while (index >= 0) { + spans.forEach { + spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + index = indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase); + } + } + return spannable +} + +/** + * Returns a new read-only list only of those given elements, that are not empty. + * Applies for CharSequence and descendants. + */ +fun listOfNotEmpty(vararg elements: T): List = elements.filterNot { it.isEmpty() } + +fun List.concat(delimiter: CharSequence? = null): CharSequence { + if (this.isEmpty()) { + return "" + } + + if (this.size == 1) { + return this[0] ?: "" + } + + var spanned = delimiter is Spanned + if (!spanned) { + for (piece in this) { + if (piece is Spanned) { + spanned = true + break + } + } + } + + var first = true + if (spanned) { + val ssb = SpannableStringBuilder() + for (piece in this) { + if (piece == null) + continue + if (!first && delimiter != null) + ssb.append(delimiter) + first = false + ssb.append(piece) + } + return SpannedString(ssb) + } else { + val sb = StringBuilder() + for (piece in this) { + if (piece == null) + continue + if (!first && delimiter != null) + sb.append(delimiter) + first = false + sb.append(piece) + } + return sb.toString() + } +} + +fun TextView.setText(@StringRes resid: Int, vararg formatArgs: Any) { + text = context.getString(resid, *formatArgs) +} + +fun JsonObject(vararg properties: Pair): JsonObject { + return JsonObject().apply { + for (property in properties) { + when (property.second) { + is JsonElement -> add(property.first, property.second as JsonElement?) + is String -> addProperty(property.first, property.second as String?) + is Char -> addProperty(property.first, property.second as Char?) + is Number -> addProperty(property.first, property.second as Number?) + is Boolean -> addProperty(property.first, property.second as Boolean?) + } + } + } +} + +fun JsonArray(vararg properties: Any?): JsonArray { + return JsonArray().apply { + for (property in properties) { + when (property) { + is JsonElement -> add(property as JsonElement?) + is String -> add(property as String?) + is Char -> add(property as Char?) + is Number -> add(property as Number?) + is Boolean -> add(property as Boolean?) + } + } + } +} + +fun Bundle(vararg properties: Pair): Bundle { + return Bundle().apply { + for (property in properties) { + when (property.second) { + is String -> putString(property.first, property.second as String?) + is Char -> putChar(property.first, property.second as Char) + is Int -> putInt(property.first, property.second as Int) + is Long -> putLong(property.first, property.second as Long) + is Float -> putFloat(property.first, property.second as Float) + is Short -> putShort(property.first, property.second as Short) + is Double -> putDouble(property.first, property.second as Double) + is Boolean -> putBoolean(property.first, property.second as Boolean) + } + } + } +} + +fun JsonArray?.isNullOrEmpty(): Boolean = (this?.size() ?: 0) == 0 +fun JsonArray.isEmpty(): Boolean = this.size() == 0 +operator fun JsonArray.plusAssign(o: JsonElement) = this.add(o) +operator fun JsonArray.plusAssign(o: String) = this.add(o) +operator fun JsonArray.plusAssign(o: Char) = this.add(o) +operator fun JsonArray.plusAssign(o: Number) = this.add(o) +operator fun JsonArray.plusAssign(o: Boolean) = this.add(o) + +@Suppress("UNCHECKED_CAST") +inline fun T.onClick(crossinline onClickListener: (v: T) -> Unit) { + setOnClickListener { v: View -> + onClickListener(v as T) + } +} + +@Suppress("UNCHECKED_CAST") +inline fun T.onChange(crossinline onChangeListener: (v: T, isChecked: Boolean) -> Unit) { + setOnCheckedChangeListener { buttonView, isChecked -> + onChangeListener(buttonView as T, isChecked) + } +} + +fun LiveData.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer) { + observe(lifecycleOwner, object : Observer { + override fun onChanged(t: T?) { + observer.onChanged(t) + removeObserver(this) + } + }) +} + +/** + * Convert a value in dp to pixels. + */ +val Int.dp: Int + get() = (this * Resources.getSystem().displayMetrics.density).toInt() +/** + * Convert a value in pixels to dp. + */ +val Int.px: Int + get() = (this / Resources.getSystem().displayMetrics.density).toInt() + +@ColorInt +fun @receiver:AttrRes Int.resolveAttr(context: Context?): Int { + val typedValue = TypedValue() + context?.theme?.resolveAttribute(this, typedValue, true) + return typedValue.data +} +@ColorInt +fun @receiver:ColorRes Int.resolveColor(context: Context): Int { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + context.resources.getColor(this, context.theme) + } + else { + context.resources.getColor(this) + } +} +fun @receiver:DrawableRes Int.resolveDrawable(context: Context): Drawable { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + context.resources.getDrawable(this, context.theme) + } + else { + context.resources.getDrawable(this) + } +} + +fun View.findParentById(targetId: Int): View? { + if (id == targetId) { + return this + } + val viewParent = this.parent ?: return null + if (viewParent is View) { + return viewParent.findParentById(targetId) + } + return null +} + +fun CoroutineScope.startCoroutineTimer(delayMillis: Long = 0, repeatMillis: Long = 0, action: () -> Unit) = launch { + delay(delayMillis) + if (repeatMillis > 0) { + while (true) { + action() + delay(repeatMillis) + } + } else { + action() + } +} + +operator fun Time?.compareTo(other: Time?): Int { + if (this == null && other == null) + return 0 + if (this == null) + return -1 + if (other == null) + return 1 + return this.compareTo(other) +} + +operator fun StringBuilder.plusAssign(str: String?) { + this.append(str) +} + +fun Context.timeTill(time: Int, delimiter: String = " ", countInSeconds: Boolean = false): String { + val parts = mutableListOf>() + + val hours = time / 3600 + val minutes = (time - hours*3600) / 60 + val seconds = time - minutes*60 - hours*3600 + + if (!countInSeconds) { + var prefixAdded = false + if (hours > 0) { + if (!prefixAdded) parts += R.plurals.time_till_text to hours + prefixAdded = true + parts += R.plurals.time_till_hours to hours + } + if (minutes > 0) { + if (!prefixAdded) parts += R.plurals.time_till_text to minutes + prefixAdded = true + parts += R.plurals.time_till_minutes to minutes + } + if (hours == 0 && minutes < 10) { + if (!prefixAdded) parts += R.plurals.time_till_text to seconds + prefixAdded = true + parts += R.plurals.time_till_seconds to seconds + } + } else { + parts += R.plurals.time_till_text to time + parts += R.plurals.time_till_seconds to time + } + + return parts.joinToString(delimiter) { resources.getQuantityString(it.first, it.second, it.second) } +} + +fun Context.timeLeft(time: Int, delimiter: String = " ", countInSeconds: Boolean = false): String { + val parts = mutableListOf>() + + val hours = time / 3600 + val minutes = (time - hours*3600) / 60 + val seconds = time - minutes*60 - hours*3600 + + if (!countInSeconds) { + var prefixAdded = false + if (hours > 0) { + if (!prefixAdded) parts += R.plurals.time_left_text to hours + prefixAdded = true + parts += R.plurals.time_left_hours to hours + } + if (minutes > 0) { + if (!prefixAdded) parts += R.plurals.time_left_text to minutes + prefixAdded = true + parts += R.plurals.time_left_minutes to minutes + } + if (hours == 0 && minutes < 10) { + if (!prefixAdded) parts += R.plurals.time_left_text to seconds + prefixAdded = true + parts += R.plurals.time_left_seconds to seconds + } + } else { + parts += R.plurals.time_left_text to time + parts += R.plurals.time_left_seconds to time + } + + return parts.joinToString(delimiter) { resources.getQuantityString(it.first, it.second, it.second) } +} + +inline fun Any?.instanceOfOrNull(): T? { + return when (this) { + is T -> this + else -> null + } +} + +fun Drawable.setTintColor(color: Int): Drawable { + colorFilter = PorterDuffColorFilter( + color, + PorterDuff.Mode.SRC_ATOP + ) + return this +} + +inline fun List.ifNotEmpty(block: (List) -> Unit) { + if (!isEmpty()) + block(this) +} + +val String.firstLettersName: String + get() { + var nameShort = "" + this.split(" ").forEach { + if (it.isBlank()) + return@forEach + nameShort += it[0].toLowerCase() + } + return nameShort + } + +val Throwable.stackTraceString: String + get() { + val sw = StringWriter() + printStackTrace(PrintWriter(sw)) + return sw.toString() + } + +inline fun LongSparseArray.filter(predicate: (T) -> Boolean): List { + val destination = ArrayList() + this.forEach { _, element -> if (predicate(element)) destination.add(element) } + return destination +} + +fun CharSequence.replace(oldValue: String, newValue: CharSequence, ignoreCase: Boolean = false): CharSequence = + splitToSequence(oldValue, ignoreCase = ignoreCase).toList().concat(newValue) + +fun Int.toColorStateList(): ColorStateList { + val states = arrayOf( + intArrayOf( android.R.attr.state_enabled ), + intArrayOf(-android.R.attr.state_enabled ), + intArrayOf(-android.R.attr.state_checked ), + intArrayOf( android.R.attr.state_pressed ) + ) + + val colors = intArrayOf( + this, + this, + this, + this + ) + + return ColorStateList(states, colors); +} + +fun SpannableStringBuilder.appendText(text: CharSequence): SpannableStringBuilder { + append(text) + return this +} +fun SpannableStringBuilder.appendSpan(text: CharSequence, what: Any, flags: Int): SpannableStringBuilder { + val start: Int = length + append(text) + setSpan(what, start, length, flags) + return this +} + +fun joinNotNullStrings(delimiter: String = "", vararg parts: String?): String { + var first = true + val sb = StringBuilder() + for (part in parts) { + if (part == null) + continue + if (!first) + sb += delimiter + first = false + sb += part + } + return sb.toString() +} + +fun String.notEmptyOrNull(): String? { + return if (isEmpty()) + null + else + this +} + +fun String.base64Encode(): String { + return encodeToString(toByteArray(), NO_WRAP) +} +fun ByteArray.base64Encode(): String { + return encodeToString(this, NO_WRAP) +} +fun String.base64Decode(): ByteArray { + return Base64.decode(this, Base64.DEFAULT) +} +fun String.base64DecodeToString(): String { + return Base64.decode(this, Base64.DEFAULT).toString(Charset.defaultCharset()) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index e617b152..726d79d1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -6,78 +6,93 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.PackageManager import android.graphics.BitmapFactory import android.graphics.drawable.BitmapDrawable -import android.os.* +import android.os.AsyncTask +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.provider.Settings import android.util.Log import android.view.Gravity import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.Observer -import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial -import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont -import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem -import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem -import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata.* -import pl.szczodrzynski.edziennik.utils.Themes -import pl.szczodrzynski.navlib.SystemBarsUtil.Companion.COLOR_HALF_TRANSPARENT -import pl.szczodrzynski.navlib.bottomsheet.NavBottomSheet -import pl.szczodrzynski.navlib.drawer.NavDrawer -import pl.szczodrzynski.navlib.drawer.items.DrawerPrimaryItem -import pl.szczodrzynski.navlib.drawer.items.withAppTitle import androidx.appcompat.widget.PopupMenu import androidx.core.graphics.ColorUtils +import androidx.lifecycle.Observer import androidx.navigation.NavOptions +import androidx.recyclerview.widget.RecyclerView import com.danimahardhika.cafebar.CafeBar +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.mikepenz.iconics.IconicsColor import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsSize +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont import com.mikepenz.materialdrawer.model.DividerDrawerItem import com.mikepenz.materialdrawer.model.ProfileDrawerItem +import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem +import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem import com.mikepenz.materialdrawer.model.interfaces.IProfile -import me.zhanghai.android.materialprogressbar.internal.ThemeUtils +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import pl.droidsonroids.gif.GifDrawable -import pl.szczodrzynski.edziennik.App.APP_URL -import pl.szczodrzynski.edziennik.data.api.AppError -import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface.* -import pl.szczodrzynski.edziennik.data.api.interfaces.SyncCallback +import pl.szczodrzynski.edziennik.data.api.events.* +import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing +import pl.szczodrzynski.edziennik.data.api.task.EdziennikTask +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata.* import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding -import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore -import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile -import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull +import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent +import pl.szczodrzynski.edziennik.sync.SyncWorker import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog -import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkFragment -import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesDetailsFragment -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment -import pl.szczodrzynski.edziennik.utils.models.NavTarget -import pl.szczodrzynski.edziennik.network.ServerRequest -import pl.szczodrzynski.edziennik.sync.SyncJob +import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog +import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog import pl.szczodrzynski.edziennik.ui.modules.agenda.AgendaFragment import pl.szczodrzynski.edziennik.ui.modules.announcements.AnnouncementsFragment import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceFragment import pl.szczodrzynski.edziennik.ui.modules.base.DebugFragment +import pl.szczodrzynski.edziennik.ui.modules.base.MainSnackbar +import pl.szczodrzynski.edziennik.ui.modules.behaviour.BehaviourFragment +import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackFragment import pl.szczodrzynski.edziennik.ui.modules.feedback.HelpFragment -import pl.szczodrzynski.edziennik.ui.modules.grades.editor.GradesEditorFragment import pl.szczodrzynski.edziennik.ui.modules.grades.GradesFragment +import pl.szczodrzynski.edziennik.ui.modules.grades.editor.GradesEditorFragment import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment -import pl.szczodrzynski.edziennik.ui.modules.behaviour.BehaviourFragment +import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkFragment +import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity +import pl.szczodrzynski.edziennik.ui.modules.messages.MessageFragment +import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesComposeFragment +import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment +import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesListFragment import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsFragment import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsNewFragment -import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment +import pl.szczodrzynski.edziennik.ui.modules.timetable.v2.TimetableFragment +import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch +import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.dpToPx +import pl.szczodrzynski.edziennik.utils.appManagerIntentList +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.NavTarget import pl.szczodrzynski.navlib.* +import pl.szczodrzynski.navlib.SystemBarsUtil.Companion.COLOR_HALF_TRANSPARENT +import pl.szczodrzynski.navlib.bottomsheet.NavBottomSheet import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem +import pl.szczodrzynski.navlib.drawer.NavDrawer +import pl.szczodrzynski.navlib.drawer.items.DrawerPrimaryItem +import pl.szczodrzynski.navlib.drawer.items.withAppTitle import java.io.File import java.io.IOException import java.util.* - +import kotlin.math.roundToInt class MainActivity : AppCompatActivity() { companion object { @@ -109,6 +124,8 @@ class MainActivity : AppCompatActivity() { const val TARGET_HELP = 502 const val TARGET_FEEDBACK = 120 const val TARGET_MESSAGES_DETAILS = 503 + const val TARGET_MESSAGES_COMPOSE = 504 + const val TARGET_WEB_PUSH = 140 const val HOME_ID = DRAWER_ITEM_HOME @@ -118,7 +135,7 @@ class MainActivity : AppCompatActivity() { // home item list += NavTarget(DRAWER_ITEM_HOME, R.string.menu_home_page, HomeFragment::class) .withTitle(R.string.app_name) - .withIcon(CommunityMaterial.Icon2.cmd_home) + .withIcon(CommunityMaterial.Icon2.cmd_home_outline) .isInDrawer(true) .isStatic(true) .withPopToHome(false) @@ -129,50 +146,50 @@ class MainActivity : AppCompatActivity() { .isInDrawer(true) list += NavTarget(DRAWER_ITEM_AGENDA, R.string.menu_agenda, AgendaFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_calendar) + .withIcon(CommunityMaterial.Icon.cmd_calendar_outline) .withBadgeTypeId(TYPE_EVENT) .isInDrawer(true) list += NavTarget(DRAWER_ITEM_GRADES, R.string.menu_grades, GradesFragment::class) - .withIcon(CommunityMaterial.Icon2.cmd_numeric_5_box) + .withIcon(CommunityMaterial.Icon2.cmd_numeric_5_box_outline) .withBadgeTypeId(TYPE_GRADE) .isInDrawer(true) list += NavTarget(DRAWER_ITEM_MESSAGES, R.string.menu_messages, MessagesFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_email) + .withIcon(CommunityMaterial.Icon.cmd_email_outline) .withBadgeTypeId(TYPE_MESSAGE) .isInDrawer(true) list += NavTarget(DRAWER_ITEM_HOMEWORK, R.string.menu_homework, HomeworkFragment::class) - .withIcon(SzkolnyFont.Icon.szf_file_document_edit) + .withIcon(SzkolnyFont.Icon.szf_notebook_outline) .withBadgeTypeId(TYPE_HOMEWORK) .isInDrawer(true) list += NavTarget(DRAWER_ITEM_BEHAVIOUR, R.string.menu_notices, BehaviourFragment::class) - .withIcon(CommunityMaterial.Icon2.cmd_message_alert) + .withIcon(CommunityMaterial.Icon.cmd_emoticon_outline) .withBadgeTypeId(TYPE_NOTICE) .isInDrawer(true) list += NavTarget(DRAWER_ITEM_ATTENDANCE, R.string.menu_attendance, AttendanceFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_calendar_remove) + .withIcon(CommunityMaterial.Icon.cmd_calendar_remove_outline) .withBadgeTypeId(TYPE_ATTENDANCE) .isInDrawer(true) list += NavTarget(DRAWER_ITEM_ANNOUNCEMENTS, R.string.menu_announcements, AnnouncementsFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_bulletin_board) + .withIcon(CommunityMaterial.Icon.cmd_bullhorn_outline) .withBadgeTypeId(TYPE_ANNOUNCEMENT) .isInDrawer(true) // static drawer items list += NavTarget(DRAWER_ITEM_NOTIFICATIONS, R.string.menu_notifications, NotificationsFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_bell_ring) + .withIcon(CommunityMaterial.Icon.cmd_bell_ring_outline) .isInDrawer(true) .isStatic(true) .isBelowSeparator(true) list += NavTarget(DRAWER_ITEM_SETTINGS, R.string.menu_settings, SettingsNewFragment::class) - .withIcon(CommunityMaterial.Icon2.cmd_settings) + .withIcon(CommunityMaterial.Icon2.cmd_settings_outline) .isInDrawer(true) .isStatic(true) .isBelowSeparator(true) @@ -191,7 +208,7 @@ class MainActivity : AppCompatActivity() { .isInProfileList(false) list += NavTarget(DRAWER_PROFILE_SYNC_ALL, R.string.menu_sync_all, null) - .withIcon(CommunityMaterial.Icon2.cmd_sync) + .withIcon(CommunityMaterial.Icon.cmd_download_outline) .isInProfileList(true) @@ -199,7 +216,9 @@ class MainActivity : AppCompatActivity() { list += NavTarget(TARGET_GRADES_EDITOR, R.string.menu_grades_editor, GradesEditorFragment::class) list += NavTarget(TARGET_HELP, R.string.menu_help, HelpFragment::class) list += NavTarget(TARGET_FEEDBACK, R.string.menu_feedback, FeedbackFragment::class) - list += NavTarget(TARGET_MESSAGES_DETAILS, R.string.menu_message, MessagesDetailsFragment::class) + 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) list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class) list @@ -210,6 +229,8 @@ class MainActivity : AppCompatActivity() { val navView: NavView by lazy { b.navView } val drawer: NavDrawer by lazy { navView.drawer } val bottomSheet: NavBottomSheet by lazy { navView.bottomSheet } + val mainSnackbar: MainSnackbar by lazy { MainSnackbar(this) } + val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) } val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout } @@ -219,10 +240,11 @@ class MainActivity : AppCompatActivity() { private val fragmentManager by lazy { supportFragmentManager } private lateinit var navTarget: NavTarget - private val navTargetId + private var navArguments: Bundle? = null + val navTargetId get() = navTarget.id - private val navBackStack = mutableListOf() + private val navBackStack = mutableListOf>() private var navLoading = true /* ____ _____ _ @@ -236,8 +258,17 @@ class MainActivity : AppCompatActivity() { setTheme(Themes.appTheme) + app.config.ui.language?.let { + setLanguage(it) + } + setContentView(b.root) + Log.d(TAG, Signing.appPassword) + + mainSnackbar.setCoordinator(b.navView.coordinator, b.navView.bottomBar) + errorSnackbar.setCoordinator(b.navView.coordinator, b.navView.bottomBar) + navLoading = true b.navView.apply { @@ -293,10 +324,10 @@ class MainActivity : AppCompatActivity() { } drawer.apply { - setAccountHeaderBackground(app.appConfig.headerBackground) + setAccountHeaderBackground(app.config.ui.headerBackground) drawerProfileListEmptyListener = { - app.appConfig.loginFinished = false + app.config.loginFinished = false app.saveConfig("loginFinished") profileListEmptyListener() } @@ -321,7 +352,7 @@ class MainActivity : AppCompatActivity() { drawerProfileSettingClickListener = this@MainActivity.profileSettingClickListener miniDrawerVisibleLandscape = null - miniDrawerVisiblePortrait = app.appConfig.miniDrawerVisible + miniDrawerVisiblePortrait = app.config.ui.miniMenuVisible } } @@ -337,7 +368,7 @@ class MainActivity : AppCompatActivity() { if (!profileListEmpty) { handleIntent(intent?.extras) } - app.db.profileDao().getAllFull().observe(this, Observer { profiles -> + app.db.profileDao().allFull.observe(this, Observer { profiles -> // TODO fix weird -1 profiles ??? profiles.removeAll { it.id < 0 } drawer.setProfileList(profiles) @@ -354,7 +385,7 @@ class MainActivity : AppCompatActivity() { if (app.profile != null) setDrawerItems() - app.db.metadataDao().getUnreadCounts().observe(this, Observer { unreadCounters -> + app.db.metadataDao().unreadCounts.observe(this, Observer { unreadCounters -> unreadCounters.map { it.type = it.thingType } @@ -363,60 +394,63 @@ class MainActivity : AppCompatActivity() { b.swipeRefreshLayout.isEnabled = true b.swipeRefreshLayout.setOnRefreshListener { this.syncCurrentFeature() } + b.swipeRefreshLayout.setColorSchemeResources( + R.color.md_blue_500, + R.color.md_amber_500, + R.color.md_green_500 + ) isStoragePermissionGranted() - SyncJob.schedule(app) + SyncWorker.scheduleNext(app) // APP BACKGROUND - if (app.appConfig.appBackground != null) { + if (app.config.ui.appBackground != null) { try { - var bg = app.appConfig.appBackground - val bgDir = File(Environment.getExternalStoragePublicDirectory("Szkolny.eu"), "bg") - if (bgDir.exists()) { - val files = bgDir.listFiles() - val r = Random() - val i = r.nextInt(files.size) - bg = files[i].toString() - } - val linearLayout = b.root - if (bg.endsWith(".gif")) { - linearLayout.background = GifDrawable(bg) - } else { - linearLayout.background = BitmapDrawable.createFromPath(bg) + app.config.ui.appBackground?.let { + var bg = it + val bgDir = File(Environment.getExternalStoragePublicDirectory("Szkolny.eu"), "bg") + if (bgDir.exists()) { + val files = bgDir.listFiles() + val r = Random() + val i = r.nextInt(files.size) + bg = files[i].toString() + } + val linearLayout = b.root + if (bg.endsWith(".gif")) { + linearLayout.background = GifDrawable(bg) + } else { + linearLayout.background = BitmapDrawable.createFromPath(bg) + } } } catch (e: IOException) { e.printStackTrace() } } + // IT'S WINTER MY DUDES + val today = Date.getToday() + if ((today.month == 12 || today.month == 1) && app.config.ui.snowfall) { + b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false)) + } + // WHAT'S NEW DIALOG - if (app.appConfig.lastAppVersion != BuildConfig.VERSION_CODE) { - ServerRequest(app, app.requestScheme + APP_URL + "main.php?just_updated", "MainActivity/JU") - .run { e, result -> - Handler(Looper.getMainLooper()).post { - try { - ChangelogDialog().show(supportFragmentManager, "whats_new") - } catch (e2: Exception) { - e2.printStackTrace() - } - } - } - if (app.appConfig.lastAppVersion < 170) { + if (app.config.appVersion < BuildConfig.VERSION_CODE) { + ChangelogDialog(this) + if (app.config.appVersion < 170) { //Intent intent = new Intent(this, ChangelogIntroActivity.class); //startActivity(intent); } else { - app.appConfig.lastAppVersion = BuildConfig.VERSION_CODE - app.saveConfig("lastAppVersion") + app.config.appVersion = BuildConfig.VERSION_CODE } } // RATE SNACKBAR - if (app.appConfig.appRateSnackbarTime != 0L && app.appConfig.appRateSnackbarTime <= System.currentTimeMillis()) { + if (app.config.appRateSnackbarTime != 0L && app.config.appRateSnackbarTime <= System.currentTimeMillis()) { navView.coordinator.postDelayed({ CafeBar.builder(this) .content(R.string.rate_snackbar_text) - .icon(IconicsDrawable(this).icon(CommunityMaterial.Icon2.cmd_star).size(IconicsSize.dp(20)).color(IconicsColor.colorInt(Themes.getPrimaryTextColor(this)))) + .icon(IconicsDrawable(this).icon(CommunityMaterial.Icon2.cmd_star_outline).size(IconicsSize.dp(20)).color(IconicsColor.colorInt(Themes.getPrimaryTextColor(this)))) .positiveText(R.string.rate_snackbar_positive) .positiveColor(-0xb350b0) .negativeText(R.string.rate_snackbar_negative) @@ -426,20 +460,17 @@ class MainActivity : AppCompatActivity() { .onPositive { cafeBar -> Utils.openGooglePlay(this) cafeBar.dismiss() - app.appConfig.appRateSnackbarTime = 0 - app.saveConfig("appRateSnackbarTime") + app.config.appRateSnackbarTime = 0 } .onNegative { cafeBar -> Toast.makeText(this, "Szkoda, opinie innych pomagają mi rozwijać aplikację.", Toast.LENGTH_LONG).show() cafeBar.dismiss() - app.appConfig.appRateSnackbarTime = 0 - app.saveConfig("appRateSnackbarTime") + app.config.appRateSnackbarTime = 0 } .onNeutral { cafeBar -> Toast.makeText(this, "OK", Toast.LENGTH_LONG).show() cafeBar.dismiss() - app.appConfig.appRateSnackbarTime = System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000 - app.saveConfig("appRateSnackbarTime") + app.config.appRateSnackbarTime = System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000 } .autoDismiss(false) .swipeToDismiss(true) @@ -453,27 +484,34 @@ class MainActivity : AppCompatActivity() { bottomSheet.appendItems( BottomSheetPrimaryItem(false) .withTitle(R.string.menu_sync) - .withIcon(CommunityMaterial.Icon2.cmd_sync) + .withIcon(CommunityMaterial.Icon.cmd_download_outline) .withOnClickListener(View.OnClickListener { bottomSheet.close() - app.apiEdziennik.guiSyncFeature(app, this, App.profileId, R.string.sync_dialog_title, R.string.sync_dialog_text, R.string.sync_done, fragmentToFeature(navTargetId)) + SyncViewListDialog(this, navTargetId) }), BottomSheetSeparatorItem(false), BottomSheetPrimaryItem(false) .withTitle(R.string.menu_settings) - .withIcon(CommunityMaterial.Icon2.cmd_settings) + .withIcon(CommunityMaterial.Icon2.cmd_settings_outline) .withOnClickListener(View.OnClickListener { loadTarget(DRAWER_ITEM_SETTINGS) }), BottomSheetPrimaryItem(false) .withTitle(R.string.menu_feedback) - .withIcon(CommunityMaterial.Icon2.cmd_help_circle) + .withIcon(CommunityMaterial.Icon2.cmd_help_circle_outline) .withOnClickListener(View.OnClickListener { loadTarget(TARGET_FEEDBACK) }) ) if (App.devMode) { bottomSheet += BottomSheetPrimaryItem(false) .withTitle(R.string.menu_debug) - .withIcon(CommunityMaterial.Icon.cmd_android_debug_bridge) + .withIcon(CommunityMaterial.Icon.cmd_android_studio) .withOnClickListener(View.OnClickListener { loadTarget(DRAWER_ITEM_DEBUG) }) } + + EventBus.getDefault().register(this) + } + + override fun onDestroy() { + EventBus.getDefault().unregister(this) + super.onDestroy() } var profileListEmptyListener = { @@ -488,7 +526,7 @@ class MainActivity : AppCompatActivity() { profileListEmptyListener() } DRAWER_PROFILE_SYNC_ALL -> { - SyncJob.run(app) + EdziennikTask.sync().enqueue(this) } else -> { loadTarget(id) @@ -508,52 +546,100 @@ class MainActivity : AppCompatActivity() { fun syncCurrentFeature() { swipeRefreshLayout.isRefreshing = true Toast.makeText(this, fragmentToSyncName(navTargetId), Toast.LENGTH_SHORT).show() - val callback = object : SyncCallback { - override fun onLoginFirst(profileList: List, loginStore: LoginStore) { - - } - - override fun onSuccess(activityContext: Context, profileFull: ProfileFull) { - swipeRefreshLayout.isRefreshing = false - } - - override fun onError(activityContext: Context, error: AppError) { - swipeRefreshLayout.isRefreshing = false - app.apiEdziennik.guiShowErrorSnackbar(this@MainActivity, error) - } - - override fun onProgress(progressStep: Int) { - - } - - override fun onActionStarted(stringResId: Int) { - - } + val fragmentParam = when (navTargetId) { + DRAWER_ITEM_MESSAGES -> MessagesFragment.pageSelection + else -> 0 } - val feature = fragmentToFeature(navTargetId) - if (feature == FEATURE_ALL) { - swipeRefreshLayout.isRefreshing = false - app.apiEdziennik.guiSync(app, this, App.profileId, R.string.sync_dialog_title, R.string.sync_dialog_text, R.string.sync_done) - } else { - app.apiEdziennik.guiSyncSilent(app, this, App.profileId, callback, feature) + val arguments = when (navTargetId) { + DRAWER_ITEM_TIMETABLE -> JsonObject("weekStart" to TimetableFragment.pageSelection?.weekStart?.stringY_m_d) + else -> null + } + EdziennikTask.syncProfile( + App.profileId, + listOf(navTargetId to fragmentParam), + arguments + ).enqueue(this) + } + @Subscribe(threadMode = ThreadMode.MAIN) + fun onApiTaskStartedEvent(event: ApiTaskStartedEvent) { + swipeRefreshLayout.isRefreshing = true + if (event.profileId == App.profileId) { + navView.toolbar.apply { + subtitleFormat = null + subtitleFormatWithUnread = null + subtitle = getString(R.string.toolbar_subtitle_syncing) + } } } - private fun fragmentToFeature(currentFragment: Int): Int { - return when (currentFragment) { - DRAWER_ITEM_TIMETABLE -> FEATURE_TIMETABLE - DRAWER_ITEM_AGENDA -> FEATURE_AGENDA - DRAWER_ITEM_GRADES -> FEATURE_GRADES - DRAWER_ITEM_HOMEWORK -> FEATURE_HOMEWORK - DRAWER_ITEM_BEHAVIOUR -> FEATURE_NOTICES - DRAWER_ITEM_ATTENDANCE -> FEATURE_ATTENDANCE - DRAWER_ITEM_MESSAGES -> when (MessagesFragment.pageSelection) { - 1 -> FEATURE_MESSAGES_OUTBOX - else -> FEATURE_MESSAGES_INBOX + @Subscribe(threadMode = ThreadMode.MAIN) + fun onApiTaskProgressEvent(event: ApiTaskProgressEvent) { + if (event.profileId == App.profileId) { + navView.toolbar.apply { + subtitleFormat = null + subtitleFormatWithUnread = null + subtitle = if (event.progress < 0f) + event.progressText ?: "" + else + getString(R.string.toolbar_subtitle_syncing_format, event.progress.roundToInt(), event.progressText ?: "") + } - DRAWER_ITEM_ANNOUNCEMENTS -> FEATURE_ANNOUNCEMENTS - else -> FEATURE_ALL } } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onApiTaskFinishedEvent(event: ApiTaskFinishedEvent) { + if (event.profileId == App.profileId) { + navView.toolbar.apply { + subtitleFormat = R.string.toolbar_subtitle + subtitleFormatWithUnread = R.plurals.toolbar_subtitle_with_unread + subtitle = "Gotowe" + } + } + } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onApiTaskAllFinishedEvent(event: ApiTaskAllFinishedEvent) { + swipeRefreshLayout.isRefreshing = false + } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onApiTaskErrorEvent(event: ApiTaskErrorEvent) { + navView.toolbar.apply { + subtitleFormat = R.string.toolbar_subtitle + subtitleFormatWithUnread = R.plurals.toolbar_subtitle_with_unread + subtitle = "Gotowe" + } + mainSnackbar.dismiss() + errorSnackbar.addError(event.error).show() + } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onAppManagerDetectedEvent(event: AppManagerDetectedEvent) { + if (app.appConfig.dontShowAppManagerDialog) + return + MaterialAlertDialogBuilder(this) + .setTitle(R.string.app_manager_dialog_title) + .setMessage(R.string.app_manager_dialog_text) + .setPositiveButton(R.string.ok) { dialog, which -> + try { + for (intent in appManagerIntentList) { + if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) { + startActivity(intent) + } + } + } catch (e: Exception) { + try { + startActivity(Intent(Settings.ACTION_SETTINGS)) + } catch (e: Exception) { + e.printStackTrace() + Toast.makeText(this, R.string.app_manager_open_failed, Toast.LENGTH_SHORT).show() + } + } + } + .setNeutralButton(R.string.dont_ask_again) { dialog, which -> + app.appConfig.dontShowAppManagerDialog = true + app.saveConfig("dontShowAppManagerDialog") + } + .setCancelable(false) + .show() + } + private fun fragmentToSyncName(currentFragment: Int): Int { return when (currentFragment) { DRAWER_ITEM_TIMETABLE -> R.string.sync_feature_timetable @@ -584,11 +670,11 @@ class MainActivity : AppCompatActivity() { } private fun handleIntent(extras: Bundle?) { - Log.d(TAG, "handleIntent() {") + d(TAG, "handleIntent() {") extras?.keySet()?.forEach { key -> - Log.d(TAG, " \"$key\": "+extras.get(key)) + d(TAG, " \"$key\": "+extras.get(key)) } - Log.d(TAG, "}") + d(TAG, "}") if (extras?.containsKey("reloadProfileId") == true) { val reloadProfileId = extras.getInt("reloadProfileId", -1) @@ -617,29 +703,33 @@ class MainActivity : AppCompatActivity() { }*/ if (navLoading) { - navLoading = false b.fragment.removeAllViews() if (intentTargetId == -1) intentTargetId = HOME_ID } when { - app.profile == null -> { + app.profile == null || app.profile.id == -1 -> { if (intentProfileId == -1) intentProfileId = app.appSharedPrefs.getInt("current_profile_id", 1) - loadProfile(intentProfileId, intentTargetId) + loadProfile(intentProfileId, intentTargetId, extras) } intentProfileId != -1 -> { - loadProfile(intentProfileId, intentTargetId) + if (app.profile.id != intentProfileId) + loadProfile(intentProfileId, intentTargetId, extras) + else + loadTarget(intentTargetId, extras) } intentTargetId != -1 -> { drawer.currentProfile = app.profile.id - loadTarget(intentTargetId, extras) + if (navTargetId != intentTargetId || navLoading) + loadTarget(intentTargetId, extras) } else -> { drawer.currentProfile = app.profile.id } } + navLoading = false } override fun recreate() { @@ -687,7 +777,7 @@ class MainActivity : AppCompatActivity() { finish() } else { - if (!app.appConfig.loginFinished) + if (!app.config.loginFinished) finish() else { handleIntent(data?.extras) @@ -712,7 +802,7 @@ class MainActivity : AppCompatActivity() { fun loadProfile(id: Int) = loadProfile(id, navTargetId) fun loadProfile(id: Int, arguments: Bundle?) = loadProfile(id, navTargetId, arguments) fun loadProfile(id: Int, drawerSelection: Int, arguments: Bundle? = null) { - Log.d("NavDebug", "loadProfile(id = $id, drawerSelection = $drawerSelection)") + //d("NavDebug", "loadProfile(id = $id, drawerSelection = $drawerSelection)") if (app.profile != null && App.profileId == id) { drawer.currentProfile = app.profile.id loadTarget(drawerSelection, arguments) @@ -720,17 +810,24 @@ class MainActivity : AppCompatActivity() { } AsyncTask.execute { app.profileLoadById(id) + MessagesFragment.pageSelection = -1 + MessagesListFragment.tapPositions = intArrayOf(RecyclerView.NO_POSITION, RecyclerView.NO_POSITION) + MessagesListFragment.topPositions = intArrayOf(RecyclerView.NO_POSITION, RecyclerView.NO_POSITION) + MessagesListFragment.bottomPositions = intArrayOf(RecyclerView.NO_POSITION, RecyclerView.NO_POSITION) this.runOnUiThread { if (app.profile == null) { LoginActivity.firstCompleted = false - if (app.appConfig.loginFinished) { + if (app.config.loginFinished) { // this shouldn't run profileListEmptyListener() } } else { setDrawerItems() - drawer.currentProfile = app.profile.id + // the drawer profile is updated automatically when the drawer item is clicked + // update it manually when switching profiles from other source + //if (drawer.currentProfile != app.profile.id) + drawer.currentProfile = app.profile.id loadTarget(drawerSelection, arguments) } } @@ -742,7 +839,7 @@ class MainActivity : AppCompatActivity() { loadId = DRAWER_ITEM_HOME } val target = navTargetList - .singleOrNull { it.id == loadId } + .firstOrNull { it.id == loadId } if (target == null) { Toast.makeText(this, getString(R.string.error_invalid_fragment, id), Toast.LENGTH_LONG).show() loadTarget(navTargetList.first(), arguments) @@ -752,7 +849,7 @@ class MainActivity : AppCompatActivity() { } } private fun loadTarget(target: NavTarget, arguments: Bundle? = null) { - Log.d("NavDebug", "loadItem(id = ${target.id})") + d("NavDebug", "loadTarget(target = $target, arguments = $arguments)") bottomSheet.close() bottomSheet.removeAllContextual() @@ -765,7 +862,7 @@ class MainActivity : AppCompatActivity() { navView.bottomBar.fabExtended = false navView.bottomBar.setFabOnClickListener(null) - Log.d("NavDebug", "Navigating from ${navTarget.fragmentClass?.java?.simpleName} to ${target.fragmentClass?.java?.simpleName}") + d("NavDebug", "Navigating from ${navTarget.fragmentClass?.java?.simpleName} to ${target.fragmentClass?.java?.simpleName}") val fragment = target.fragmentClass?.java?.newInstance() ?: return fragment.arguments = arguments @@ -779,7 +876,7 @@ class MainActivity : AppCompatActivity() { ) } else { - navBackStack.lastIndexOf(target).let { + navBackStack.keys().lastIndexOf(target).let { if (it == -1) return@let target // pop the back stack up until that target @@ -810,8 +907,9 @@ class MainActivity : AppCompatActivity() { R.anim.task_open_enter, R.anim.task_open_exit ) - navBackStack.add(navTarget) + navBackStack.add(navTarget to arguments) navTarget = target + navArguments = arguments } } @@ -824,9 +922,9 @@ class MainActivity : AppCompatActivity() { } } - Log.d("NavDebug", "Current fragment ${navTarget.fragmentClass?.java?.simpleName}, pop to home ${navTarget.popToHome}, back stack:") + d("NavDebug", "Current fragment ${navTarget.fragmentClass?.java?.simpleName}, pop to home ${navTarget.popToHome}, back stack:") navBackStack.forEachIndexed { index, target2 -> - Log.d("NavDebug", " - $index: ${target2.fragmentClass?.java?.simpleName}") + d("NavDebug", " - $index: ${target2.first.fragmentClass?.java?.simpleName}") } transaction.replace(R.id.fragment, fragment) @@ -851,11 +949,18 @@ class MainActivity : AppCompatActivity() { return false } // TODO back stack argument support - if (navTarget.popToHome) { - loadTarget(HOME_ID) - } - else { - loadTarget(navBackStack.last()) + when { + navTarget.popToHome -> { + loadTarget(HOME_ID) + } + navTarget.popTo != null -> { + loadTarget(navTarget.popTo ?: HOME_ID) + } + else -> { + navBackStack.last().let { + loadTarget(it.first, it.second) + } + } } return true } @@ -870,6 +975,8 @@ class MainActivity : AppCompatActivity() { * that something has changed in the bottom sheet. */ fun gainAttention() { + if (app.config.ui.bottomSheetOpened || true) + return b.navView.postDelayed({ navView.gainAttentionOnBottomBar() }, 2000) @@ -897,7 +1004,7 @@ class MainActivity : AppCompatActivity() { val item = DrawerPrimaryItem() .withIdentifier(target.id.toLong()) .withName(target.name) - .withHiddenInMiniDrawer(!app.appConfig.miniDrawerButtonIds.contains(target.id)) + .withHiddenInMiniDrawer(!app.config.ui.miniMenuButtons.contains(target.id)) .also { if (target.description != null) it.withDescription(target.description!!) } .also { if (target.icon != null) it.withIcon(target.icon!!) } .also { if (target.title != null) it.withAppTitle(getString(target.title!!)) } @@ -917,7 +1024,7 @@ class MainActivity : AppCompatActivity() { } fun setDrawerItems() { - Log.d("NavDebug", "setDrawerItems() app.profile = ${app.profile ?: "null"}") + d("NavDebug", "setDrawerItems() app.profile = ${app.profile ?: "null"}") val drawerItems = arrayListOf>() val drawerProfiles = arrayListOf() @@ -975,7 +1082,7 @@ class MainActivity : AppCompatActivity() { } loadTarget(DRAWER_ITEM_SETTINGS, null) } else if (item.itemId == 2) { - app.apiEdziennik.guiRemoveProfile(this@MainActivity, profileId, profile.name?.getText(this).toString()) + ProfileRemoveDialog(this, profileId, profile.name?.getText(this)?.toString() ?: "?") } true } @@ -986,30 +1093,14 @@ class MainActivity : AppCompatActivity() { private var targetHomeId: Int = -1 override fun onBackPressed() { if (!b.navView.onBackPressed()) { - - navigateUp() - - /*val currentDestinationId = navController.currentDestination?.id - - if (if (targetHomeId != -1 && targetPopToHomeList.contains(navController.currentDestination?.id)) { - if (!navController.popBackStack(targetHomeId, false)) { - navController.navigateUp() - } - true - } else { - navController.navigateUp() - }) { - val currentId = navController.currentDestination?.id ?: -1 - val drawerSelection = navTargetList - .singleOrNull { - it.navGraphId == currentId - }?.also { - navView.toolbar.setTitle(it.title ?: it.name) - }?.id ?: -1 - drawer.setSelection(drawerSelection, false) + if (App.getConfig().ui.openDrawerOnBackPressed) { + b.navView.drawer.toggle() } else { - super.onBackPressed() - }*/ + navigateUp() + } } } + + fun snackbar(text: String, actionText: String? = null, onClick: (() -> Unit)? = null) = mainSnackbar.snackbar(text, actionText, onClick) + fun snackbarDismiss() = mainSnackbar.dismiss() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Notifier.java b/app/src/main/java/pl/szczodrzynski/edziennik/Notifier.java index cc2237c3..0b2d01b5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Notifier.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Notifier.java @@ -8,24 +8,22 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Build; +import android.util.Log; + import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; -import android.util.Log; import java.util.ArrayList; import java.util.Collections; import java.util.List; import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull; +import pl.szczodrzynski.edziennik.receivers.BootReceiver; import pl.szczodrzynski.edziennik.utils.models.Date; import pl.szczodrzynski.edziennik.utils.models.Time; -import pl.szczodrzynski.edziennik.receivers.BootReceiver; -import pl.szczodrzynski.edziennik.sync.SyncJob; -import pl.szczodrzynski.edziennik.sync.SyncService; import static androidx.core.app.NotificationCompat.PRIORITY_DEFAULT; import static androidx.core.app.NotificationCompat.PRIORITY_MAX; -import static pl.szczodrzynski.edziennik.sync.SyncService.ACTION_CANCEL; public class Notifier { @@ -36,14 +34,14 @@ public class Notifier { private static String CHANNEL_GET_DATA_DESC; private static final String GROUP_KEY_GET_DATA = "pl.szczodrzynski.edziennik.GET_DATA"; - private static final int ID_NOTIFICATIONS = 1337002; - private static String CHANNEL_NOTIFICATIONS_NAME; - private static String CHANNEL_NOTIFICATIONS_DESC; + public static final int ID_NOTIFICATIONS = 1337002; + public static String CHANNEL_NOTIFICATIONS_NAME; + public static String CHANNEL_NOTIFICATIONS_DESC; public static final String GROUP_KEY_NOTIFICATIONS = "pl.szczodrzynski.edziennik.NOTIFICATIONS"; - private static final int ID_NOTIFICATIONS_QUIET = 1337002; - private static String CHANNEL_NOTIFICATIONS_QUIET_NAME; - private static String CHANNEL_NOTIFICATIONS_QUIET_DESC; + public static final int ID_NOTIFICATIONS_QUIET = 1337002; + public static String CHANNEL_NOTIFICATIONS_QUIET_NAME; + public static String CHANNEL_NOTIFICATIONS_QUIET_DESC; public static final String GROUP_KEY_NOTIFICATIONS_QUIET = "pl.szczodrzynski.edziennik.NOTIFICATIONS_QUIET"; private static final int ID_UPDATES = 1337003; @@ -52,9 +50,9 @@ public class Notifier { private static final String GROUP_KEY_UPDATES = "pl.szczodrzynski.edziennik.UPDATES"; private App app; - private NotificationManager notificationManager; + public NotificationManager notificationManager; private NotificationCompat.Builder getDataNotificationBuilder; - private int notificationColor; + public int notificationColor; Notifier(App _app) { this.app = _app; @@ -71,7 +69,7 @@ public class Notifier { notificationColor = ContextCompat.getColor(app.getContext(), R.color.colorPrimary); notificationManager = (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel channelGetData = new NotificationChannel(GROUP_KEY_GET_DATA, CHANNEL_GET_DATA_NAME, NotificationManager.IMPORTANCE_LOW); + NotificationChannel channelGetData = new NotificationChannel(GROUP_KEY_GET_DATA, CHANNEL_GET_DATA_NAME, NotificationManager.IMPORTANCE_MIN); channelGetData.setDescription(CHANNEL_GET_DATA_DESC); notificationManager.createNotificationChannel(channelGetData); @@ -95,8 +93,8 @@ public class Notifier { public boolean shouldBeQuiet() { long now = Time.getNow().getInMillis(); - long start = app.appConfig.quietHoursStart; - long end = app.appConfig.quietHoursEnd; + long start = app.config.getSync().getQuietHoursStart(); + long end = app.config.getSync().getQuietHoursEnd(); if (start > end) { end += 1000 * 60 * 60 * 24; //Log.d(TAG, "Night passing"); @@ -106,16 +104,16 @@ public class Notifier { //Log.d(TAG, "Now is smaller"); } //Log.d(TAG, "Start is "+start+", now is "+now+", end is "+end); - return app.appConfig.quietHoursStart > 0 && now >= start && now <= end; + return start > 0 && now >= start && now <= end; } - private int getNotificationDefaults() { + public int getNotificationDefaults() { return (shouldBeQuiet() ? 0 : Notification.DEFAULT_ALL); } - private String getNotificationGroup() { + public String getNotificationGroup() { return shouldBeQuiet() ? GROUP_KEY_NOTIFICATIONS_QUIET : GROUP_KEY_NOTIFICATIONS; } - private int getNotificationPriority() { + public int getNotificationPriority() { return shouldBeQuiet() ? PRIORITY_DEFAULT : PRIORITY_MAX; } @@ -126,17 +124,17 @@ public class Notifier { | |__| | (_| | || (_| | | |__| | __/ |_ |_____/ \__,_|\__\__,_| \_____|\___|\_*/ public Notification notificationGetDataShow(int maxProgress) { - Intent notificationIntent = new Intent(app.getContext(), SyncService.class); + /*Intent notificationIntent = new Intent(app.getContext(), SyncService.class); notificationIntent.setAction(ACTION_CANCEL); PendingIntent pendingIntent = PendingIntent.getService(app.getContext(), 0, - notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); + notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT);*/ getDataNotificationBuilder = new NotificationCompat.Builder(app, GROUP_KEY_GET_DATA) .setSmallIcon(android.R.drawable.stat_sys_download) .setColor(notificationColor) .setContentTitle(app.getString(R.string.notification_get_data_title)) .setContentText(app.getString(R.string.notification_get_data_text)) - .addAction(R.drawable.ic_notification, app.getString(R.string.notification_get_data_cancel), pendingIntent) + //.addAction(R.drawable.ic_notification, app.getString(R.string.notification_get_data_cancel), pendingIntent) //.setGroup(GROUP_KEY_GET_DATA) .setOngoing(true) .setProgress(maxProgress, 0, false) @@ -208,10 +206,8 @@ public class Notifier { @Override protected void onHandleIntent(Intent intent) { - SyncJob.run((App) getApplication(), intent.getExtras().getInt("failedProfileId", -1), -1); - NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - assert notificationManager != null; - notificationManager.cancel(ID_GET_DATA_ERROR); + + } } @@ -315,13 +311,14 @@ public class Notifier { \____/| .__/ \__,_|\__,_|\__\___||___/ | | |*/ - public void notificationUpdatesShow(String updateVersion, String updateUrl, String updateFilename) { - if (!app.appConfig.notifyAboutUpdates) + public void notificationUpdatesShow(String updateVersion, String updateUrl, String updateFilename, boolean updateDirect) { + if (!app.config.getSync().getNotifyAboutUpdates()) return; Intent notificationIntent = new Intent(app.getContext(), BootReceiver.NotificationActionService.class) .putExtra("update_version", updateVersion) .putExtra("update_url", updateUrl) - .putExtra("update_filename", updateFilename); + .putExtra("update_filename", updateFilename) + .putExtra("update_direct", updateDirect); PendingIntent pendingIntent = PendingIntent.getService(app.getContext(), 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); @@ -343,7 +340,7 @@ public class Notifier { } public void notificationUpdatesHide() { - if (!app.appConfig.notifyAboutUpdates) + if (!app.config.getSync().getNotifyAboutUpdates()) return; notificationManager.cancel(ID_UPDATES); } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/WidgetTimetable.java b/app/src/main/java/pl/szczodrzynski/edziennik/WidgetTimetable.java deleted file mode 100644 index 36296e39..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/WidgetTimetable.java +++ /dev/null @@ -1,450 +0,0 @@ -package pl.szczodrzynski.edziennik; - -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProvider; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Build; -import android.util.SparseArray; -import android.view.View; -import android.widget.RemoteViews; - -import com.mikepenz.iconics.IconicsColor; -import com.mikepenz.iconics.IconicsDrawable; -import com.mikepenz.iconics.IconicsSize; -import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; - -import pl.szczodrzynski.edziennik.data.db.modules.events.EventFull; -import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange; -import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonFull; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile; -import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment; -import pl.szczodrzynski.edziennik.utils.models.Date; -import pl.szczodrzynski.edziennik.utils.models.ItemWidgetTimetableModel; -import pl.szczodrzynski.edziennik.utils.models.Time; -import pl.szczodrzynski.edziennik.utils.models.Week; -import pl.szczodrzynski.edziennik.widgets.WidgetConfig; -import pl.szczodrzynski.edziennik.sync.SyncJob; -import pl.szczodrzynski.edziennik.widgets.timetable.LessonDetailsActivity; -import pl.szczodrzynski.edziennik.widgets.timetable.WidgetTimetableService; - -import static pl.szczodrzynski.edziennik.ExtensionsKt.filterOutArchived; -import static pl.szczodrzynski.edziennik.data.db.modules.events.Event.TYPE_HOMEWORK; -import static pl.szczodrzynski.edziennik.utils.Utils.bs; - - -public class WidgetTimetable extends AppWidgetProvider { - - - public static final String ACTION_SYNC_DATA = "ACTION_SYNC_DATA"; - private static final String TAG = "WidgetTimetable"; - private static int modeInt = 0; - - public WidgetTimetable() { - // Start the worker thread - //HandlerThread sWorkerThread = new HandlerThread("WidgetTimetable-worker"); - //sWorkerThread.start(); - //Handler sWorkerQueue = new Handler(sWorkerThread.getLooper()); - } - - public static SparseArray> timetables = null; - - @Override - public void onReceive(Context context, Intent intent) { - if (ACTION_SYNC_DATA.equals(intent.getAction())){ - SyncJob.run((App) context.getApplicationContext()); - } - super.onReceive(context, intent); - } - - public static PendingIntent getPendingSelfIntent(Context context, String action) { - Intent intent = new Intent(context, WidgetTimetable.class); - intent.setAction(action); - return getPendingSelfIntent(context, intent); - } - public static PendingIntent getPendingSelfIntent(Context context, Intent intent) { - return PendingIntent.getBroadcast(context, 0, intent, 0); - } - - public static Bitmap drawableToBitmap (Drawable drawable) { - - if (drawable instanceof BitmapDrawable) { - return ((BitmapDrawable)drawable).getBitmap(); - } - - Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - - return bitmap; - } - - @Override - public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { - ComponentName thisWidget = new ComponentName(context, WidgetTimetable.class); - - timetables = new SparseArray<>(); - //timetables.clear(); - - App app = (App)context.getApplicationContext(); - - int[] allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget); - // There may be multiple widgets active, so update all of them - for (int appWidgetId : allWidgetIds) { - - //d(TAG, "thr "+Thread.currentThread().getName()); - - WidgetConfig widgetConfig = app.appConfig.widgetTimetableConfigs.get(appWidgetId); - if (widgetConfig == null) { - widgetConfig = new WidgetConfig(app.profileFirstId()); - app.appConfig.widgetTimetableConfigs.put(appWidgetId, widgetConfig); - app.appConfig.savePending = true; - } - - RemoteViews views; - if (widgetConfig.bigStyle) { - views = new RemoteViews(context.getPackageName(), widgetConfig.darkTheme ? R.layout.widget_timetable_dark_big : R.layout.widget_timetable_big); - } - else { - views = new RemoteViews(context.getPackageName(), widgetConfig.darkTheme ? R.layout.widget_timetable_dark : R.layout.widget_timetable); - } - - PorterDuff.Mode mode = PorterDuff.Mode.DST_IN; - /*if (widgetConfig.darkTheme) { - switch (modeInt) { - case 0: - mode = PorterDuff.Mode.ADD; - d(TAG, "ADD"); - break; - case 1: - mode = PorterDuff.Mode.DST_ATOP; - d(TAG, "DST_ATOP"); - break; - case 2: - mode = PorterDuff.Mode.DST_IN; - d(TAG, "DST_IN"); - break; - case 3: - mode = PorterDuff.Mode.DST_OUT; - d(TAG, "DST_OUT"); - break; - case 4: - mode = PorterDuff.Mode.DST_OVER; - d(TAG, "DST_OVER"); - break; - case 5: - mode = PorterDuff.Mode.LIGHTEN; - d(TAG, "LIGHTEN"); - break; - case 6: - mode = PorterDuff.Mode.MULTIPLY; - d(TAG, "MULTIPLY"); - break; - case 7: - mode = PorterDuff.Mode.OVERLAY; - d(TAG, "OVERLAY"); - break; - case 8: - mode = PorterDuff.Mode.SCREEN; - d(TAG, "SCREEN"); - break; - case 9: - mode = PorterDuff.Mode.SRC_ATOP; - d(TAG, "SRC_ATOP"); - break; - case 10: - mode = PorterDuff.Mode.SRC_IN; - d(TAG, "SRC_IN"); - break; - case 11: - mode = PorterDuff.Mode.SRC_OUT; - d(TAG, "SRC_OUT"); - break; - case 12: - mode = PorterDuff.Mode.SRC_OVER; - d(TAG, "SRC_OVER"); - break; - case 13: - mode = PorterDuff.Mode.XOR; - d(TAG, "XOR"); - break; - default: - modeInt = 0; - mode = PorterDuff.Mode.ADD; - d(TAG, "ADD"); - break; - } - }*/ - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - // this code seems to crash the launcher on >= P - float transparency = widgetConfig.opacity; //0...1 - long colorFilter = 0x01000000L * (long) (255f * transparency); - try { - final Method[] declaredMethods = Class.forName("android.widget.RemoteViews").getDeclaredMethods(); - final int len = declaredMethods.length; - if (len > 0) { - for (int m = 0; m < len; m++) { - final Method method = declaredMethods[m]; - if (method.getName().equals("setDrawableParameters")) { - method.setAccessible(true); - method.invoke(views, R.id.widgetTimetableListView, true, -1, (int) colorFilter, mode, -1); - method.invoke(views, R.id.widgetTimetableHeader, true, -1, (int) colorFilter, mode, -1); - break; - } - } - } - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } - } - - Intent refreshIntent = new Intent(context, WidgetTimetable.class); - refreshIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - refreshIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds); - PendingIntent pendingRefreshIntent = PendingIntent.getBroadcast(context, - 0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT); - views.setOnClickPendingIntent(R.id.widgetTimetableRefresh, pendingRefreshIntent); - - views.setOnClickPendingIntent(R.id.widgetTimetableSync, WidgetTimetable.getPendingSelfIntent(context, ACTION_SYNC_DATA)); - - views.setImageViewBitmap(R.id.widgetTimetableRefresh, new IconicsDrawable(context, CommunityMaterial.Icon2.cmd_refresh) - .color(IconicsColor.colorInt(Color.WHITE)) - .size(IconicsSize.dp(widgetConfig.bigStyle ? 24 : 16)).toBitmap()); - - views.setImageViewBitmap(R.id.widgetTimetableSync, new IconicsDrawable(context, CommunityMaterial.Icon2.cmd_sync) - .color(IconicsColor.colorInt(Color.WHITE)) - .size(IconicsSize.dp(widgetConfig.bigStyle ? 24 : 16)).toBitmap()); - - boolean unified = widgetConfig.profileId == -1; - - List profileList = new ArrayList<>(); - if (unified) { - profileList = app.db.profileDao().getAllNow(); - filterOutArchived(profileList); - } - else { - Profile profile = app.db.profileDao().getByIdNow(widgetConfig.profileId); - if (profile != null) { - profileList.add(profile); - } - } - - //d(TAG, "Profiles: "+ Arrays.toString(profileList.toArray())); - - if (profileList == null || profileList.size() == 0) { - views.setViewVisibility(R.id.widgetTimetableLoading, View.VISIBLE); - views.setTextViewText(R.id.widgetTimetableLoading, app.getString(R.string.widget_timetable_profile_doesnt_exist)); - } - else { - views.setViewVisibility(R.id.widgetTimetableLoading, View.GONE); - //Register profile; - - long bellSyncDiffMillis = 0; - if (app.appConfig.bellSyncDiff != null) { - bellSyncDiffMillis = app.appConfig.bellSyncDiff.hour * 60 * 60 * 1000 + app.appConfig.bellSyncDiff.minute * 60 * 1000 + app.appConfig.bellSyncDiff.second * 1000; - bellSyncDiffMillis *= app.appConfig.bellSyncMultiplier; - bellSyncDiffMillis *= -1; - } - - List lessonList = new ArrayList<>(); - - Time syncedNow = Time.fromMillis(Time.getNow().getInMillis() + bellSyncDiffMillis); - - Date today = Date.getToday(); - - int openProfileId = -1; - Date displayingDate = null; - int displayingWeekDay = 0; - if (unified) { - views.setTextViewText(R.id.widgetTimetableSubtitle, app.getString(R.string.widget_timetable_title_unified)); - } - else { - views.setTextViewText(R.id.widgetTimetableSubtitle, profileList.get(0).getName()); - openProfileId = profileList.get(0).getId(); - } - - List lessons = app.db.lessonDao().getAllWeekNow(unified ? -1 : openProfileId, today.clone().stepForward(0, 0, -today.getWeekDay()), today); - - int scrollPos = 0; - - for (Profile profile: profileList) { - Date profileDisplayingDate = HomeFragment.findDateWithLessons(profile.getId(), lessons, syncedNow, 1); - int profileDisplayingWeekDay = profileDisplayingDate.getWeekDay(); - int dayDiff = Date.diffDays(profileDisplayingDate, Date.getToday()); - - //d(TAG, "For profile "+profile.name+" displayingDate is "+profileDisplayingDate.getStringY_m_d()); - if (displayingDate == null || profileDisplayingDate.getValue() < displayingDate.getValue()) { - displayingDate = profileDisplayingDate; - displayingWeekDay = profileDisplayingWeekDay; - //d(TAG, "Setting as global dd"); - if (dayDiff == 0) { - views.setTextViewText(R.id.widgetTimetableTitle, app.getString(R.string.day_today_format, Week.getFullDayName(displayingWeekDay))); - } else if (dayDiff == 1) { - views.setTextViewText(R.id.widgetTimetableTitle, app.getString(R.string.day_tomorrow_format, Week.getFullDayName(displayingWeekDay))); - } else { - views.setTextViewText(R.id.widgetTimetableTitle, Week.getFullDayName(displayingWeekDay) + " " + profileDisplayingDate.getStringDm()); - } - } - } - - for (Profile profile: profileList) { - int pos = 0; - - List events = app.db.eventDao().getAllByDateNow(profile.getId(), displayingDate); - if (events == null) - events = new ArrayList<>(); - - if (unified) { - ItemWidgetTimetableModel separator = new ItemWidgetTimetableModel(); - separator.profileId = profile.getId(); - separator.bigStyle = widgetConfig.bigStyle; - separator.darkTheme = widgetConfig.darkTheme; - separator.separatorProfileName = profile.getName(); - lessonList.add(separator); - } - - for (LessonFull lesson : lessons) { - //d(TAG, "Profile "+profile.id+" Lesson profileId "+lesson.profileId+" weekDay "+lesson.weekDay+", "+lesson); - if (profile.getId() != lesson.profileId || displayingWeekDay != lesson.weekDay) - continue; - //d(TAG, "Not skipped"); - ItemWidgetTimetableModel model = new ItemWidgetTimetableModel(); - - model.bigStyle = widgetConfig.bigStyle; - model.darkTheme = widgetConfig.darkTheme; - - model.profileId = profile.getId(); - - model.lessonDate = displayingDate; - model.startTime = lesson.startTime; - model.endTime = lesson.endTime; - - model.lessonPassed = (syncedNow.getValue() > lesson.endTime.getValue()) && displayingWeekDay == Week.getTodayWeekDay(); - model.lessonCurrent = (Time.inRange(lesson.startTime, lesson.endTime, syncedNow)) && displayingWeekDay == Week.getTodayWeekDay(); - - if (model.lessonCurrent) { - scrollPos = pos; - } else if (model.lessonPassed) { - scrollPos = pos + 1; - } - pos++; - - model.subjectName = bs(lesson.subjectLongName); - model.classroomName = lesson.classroomName; - - model.bellSyncDiffMillis = bellSyncDiffMillis; - - if (lesson.changeId != 0) { - if (lesson.changeType == LessonChange.TYPE_CHANGE) { - model.lessonChange = true; - if (lesson.changedClassroomName()) { - model.newClassroomName = lesson.changeClassroomName; - } - - if (lesson.changedSubjectLongName()) { - model.newSubjectName = lesson.changeSubjectLongName; - } - } - if (lesson.changeType == LessonChange.TYPE_CANCELLED) { - model.lessonCancelled = true; - } - } - - for (EventFull event : events) { - if (event.startTime == null) - continue; - if (event.eventDate.getValue() == displayingDate.getValue() - && event.startTime.getValue() == lesson.startTime.getValue()) { - model.eventColors.add(event.type == TYPE_HOMEWORK ? ItemWidgetTimetableModel.EVENT_COLOR_HOMEWORK : event.getColor()); - } - } - - lessonList.add(model); - } - } - - if (lessonList.size() == 0) { - views.setViewVisibility(R.id.widgetTimetableLoading, View.VISIBLE); - views.setRemoteAdapter(R.id.widgetTimetableListView, new Intent()); - views.setTextViewText(R.id.widgetTimetableLoading, app.getString(R.string.widget_timetable_no_lessons)); - appWidgetManager.updateAppWidget(appWidgetId, views); - } - else { - views.setViewVisibility(R.id.widgetTimetableLoading, View.GONE); - - timetables.put(appWidgetId, lessonList); - //WidgetTimetableListProvider.widgetsLessons.put(appWidgetId, lessons); - //views.setRemoteAdapter(R.id.widgetTimetableListView, new Intent()); - Intent listIntent = new Intent(context, WidgetTimetableService.class); - listIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); - listIntent.setData(Uri.parse(listIntent.toUri(Intent.URI_INTENT_SCHEME))); - views.setRemoteAdapter(R.id.widgetTimetableListView, listIntent); - - // template to handle the click listener for each item - Intent intentTemplate = new Intent(context, LessonDetailsActivity.class); - // Old activities shouldn't be in the history stack - intentTemplate.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - PendingIntent pendingIntentTimetable = PendingIntent.getActivity(context, - 0, - intentTemplate, - 0); - views.setPendingIntentTemplate(R.id.widgetTimetableListView, pendingIntentTimetable); - - Intent openIntent = new Intent(context, MainActivity.class); - openIntent.setAction("android.intent.action.MAIN"); - if (!unified) { - openIntent.putExtra("profileId", openProfileId); - openIntent.putExtra("timetableDate", displayingDate.getValue()); - } - openIntent.putExtra("fragmentId", MainActivity.DRAWER_ITEM_TIMETABLE); - PendingIntent pendingOpenIntent = PendingIntent.getActivity(context, - appWidgetId, openIntent, PendingIntent.FLAG_UPDATE_CURRENT); - views.setOnClickPendingIntent(R.id.widgetTimetableHeader, pendingOpenIntent); - - if (!unified) - views.setScrollPosition(R.id.widgetTimetableListView, scrollPos); - } - } - - appWidgetManager.updateAppWidget(appWidgetId, views); - appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widgetTimetableListView); - } - //modeInt++; - } - - @Override - public void onEnabled(Context context) { - // Enter relevant functionality for when the first widget is created - } - - @Override - public void onDeleted(Context context, int[] appWidgetIds) { - App app = (App) context.getApplicationContext(); - for (int appWidgetId: appWidgetIds) { - app.appConfig.widgetTimetableConfigs.remove(appWidgetId); - } - app.saveConfig("widgetTimetableConfigs"); - } -} - diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/WidgetTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/WidgetTimetable.kt new file mode 100644 index 00000000..988e644a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/WidgetTimetable.kt @@ -0,0 +1,407 @@ +package pl.szczodrzynski.edziennik + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.util.SparseArray +import android.view.View +import android.widget.RemoteViews +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import pl.szczodrzynski.edziennik.data.api.task.EdziennikTask +import pl.szczodrzynski.edziennik.data.db.modules.events.Event.TYPE_HOMEWORK +import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.ItemWidgetTimetableModel +import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.edziennik.utils.models.Week +import pl.szczodrzynski.edziennik.widgets.WidgetConfig +import pl.szczodrzynski.edziennik.widgets.timetable.LessonDialogActivity +import pl.szczodrzynski.edziennik.widgets.timetable.WidgetTimetableService +import java.lang.reflect.InvocationTargetException + + +class WidgetTimetable : AppWidgetProvider() { + + override fun onReceive(context: Context, intent: Intent) { + if (ACTION_SYNC_DATA == intent.action) { + EdziennikTask.sync().enqueue(context) + } + super.onReceive(context, intent) + } + + private val ignoreCancelled = true + + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + val thisWidget = ComponentName(context, WidgetTimetable::class.java) + + timetables = SparseArray() + //timetables.clear(); + + val app = context.applicationContext as App + + var bellSyncDiffMillis: Long = 0 + app.config.timetable.bellSyncDiff?.let { + bellSyncDiffMillis = (it.hour * 60 * 60 * 1000 + it.minute * 60 * 1000 + it.second * 1000).toLong() + bellSyncDiffMillis *= app.config.timetable.bellSyncMultiplier.toLong() + bellSyncDiffMillis *= -1 + } + + val allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget) + + allWidgetIds?.forEach { appWidgetId -> + var widgetConfig = app.appConfig.widgetTimetableConfigs[appWidgetId] + if (widgetConfig == null) { + widgetConfig = WidgetConfig(app.profileFirstId()) + app.appConfig.widgetTimetableConfigs[appWidgetId] = widgetConfig + app.appConfig.savePending = true + } + + val views = if (widgetConfig.bigStyle) { + RemoteViews(context.packageName, if (widgetConfig.darkTheme) R.layout.widget_timetable_dark_big else R.layout.widget_timetable_big) + } else { + RemoteViews(context.packageName, if (widgetConfig.darkTheme) R.layout.widget_timetable_dark else R.layout.widget_timetable) + } + + val refreshIntent = Intent(app, WidgetTimetable::class.java) + refreshIntent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE + refreshIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds) + val pendingRefreshIntent = PendingIntent.getBroadcast(context, + 0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT) + views.setOnClickPendingIntent(R.id.widgetTimetableRefresh, pendingRefreshIntent) + + views.setOnClickPendingIntent(R.id.widgetTimetableSync, getPendingSelfIntent(context, ACTION_SYNC_DATA)) + + views.setImageViewBitmap(R.id.widgetTimetableRefresh, IconicsDrawable(context, CommunityMaterial.Icon2.cmd_refresh) + .colorInt(Color.WHITE) + .sizeDp(if (widgetConfig.bigStyle) 24 else 16).toBitmap()) + + views.setImageViewBitmap(R.id.widgetTimetableSync, IconicsDrawable(context, CommunityMaterial.Icon.cmd_download_outline) + .colorInt(Color.WHITE) + .sizeDp(if (widgetConfig.bigStyle) 24 else 16).toBitmap()) + + prepareAppWidget(app, appWidgetId, views, widgetConfig, bellSyncDiffMillis) + + appWidgetManager.updateAppWidget(appWidgetId, views) + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widgetTimetableListView) + } + } + + private fun prepareAppWidget( + app: App, + appWidgetId: Int, + views: RemoteViews, + widgetConfig: WidgetConfig, + bellSyncDiffMillis: Long + ) { + // get the current bell-synced time + val now = Time.fromMillis(Time.getNow().inMillis + bellSyncDiffMillis) + + // set the widget transparency + val mode = PorterDuff.Mode.DST_IN + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + // this code seems to crash the launcher on >= P + val transparency = widgetConfig.opacity //0...1 + val colorFilter = 0x01000000L * (255f * transparency).toLong() + try { + val declaredMethods = Class.forName("android.widget.RemoteViews").declaredMethods + val len = declaredMethods.size + if (len > 0) { + for (m in 0 until len) { + val method = declaredMethods[m] + if (method.name == "setDrawableParameters") { + method.isAccessible = true + method.invoke(views, R.id.widgetTimetableBackground, true, -1, colorFilter.toInt(), mode, -1) + method.invoke(views, R.id.widgetTimetableHeader, true, -1, colorFilter.toInt(), mode, -1) + break + } + } + } + } catch (e: ClassNotFoundException) { + e.printStackTrace() + } catch (e: InvocationTargetException) { + e.printStackTrace() + } catch (e: IllegalAccessException) { + e.printStackTrace() + } catch (e: IllegalArgumentException) { + e.printStackTrace() + } + } + + val unified = widgetConfig.profileId == -1 + + // get all profiles or one profile with the specified id + val profileList = if (unified) + app.db.profileDao().allNow.filterOutArchived() + else + listOfNotNull(app.db.profileDao().getByIdNow(widgetConfig.profileId)) + + // no profile was found + if (profileList.isEmpty()) { + views.setViewVisibility(R.id.widgetTimetableLoading, View.VISIBLE) + views.setTextViewText(R.id.widgetTimetableLoading, app.getString(R.string.widget_timetable_profile_doesnt_exist)) + return + } + + views.setViewVisibility(R.id.widgetTimetableLoading, View.GONE) + + // set lesson search bounds + val today = Date.getToday() + val searchEnd = today.clone().stepForward(0, 0, 7) + + var scrollPos = 0 + + var profileId: Int? = null + var displayingDate: Date? = null + + val models = mutableListOf() + + // get all lessons within the search bounds + val lessonList = app.db.timetableDao().getBetweenDatesNow(today, searchEnd) + + for (profile in profileList) { + + // add a profile separator with its name + if (unified) { + val separator = ItemWidgetTimetableModel() + separator.profileId = profile.id + separator.bigStyle = widgetConfig.bigStyle + separator.darkTheme = widgetConfig.darkTheme + separator.separatorProfileName = profile.name + models.add(separator) + } + + // search for lessons to display + val timetableDate = Date.getToday() + var checkedDays = 0 + var lessons = lessonList.filter { + it.profileId == profile.id + && it.displayDate == timetableDate + && it.displayEndTime > now + && !(it.isCancelled && ignoreCancelled) + } + while ((lessons.isEmpty() || lessons.none { + it.type != Lesson.TYPE_NO_LESSONS + && (it.displayDate != today + || (it.displayDate == today + && it.displayEndTime != null + && it.displayEndTime!! >= now)) + }) && checkedDays < 7) { + + timetableDate.stepForward(0, 0, 1) + lessons = lessonList.filter { + it.profileId == profile.id + && it.displayDate == timetableDate + && !(it.isCancelled && ignoreCancelled) + } + + if (lessons.isEmpty() && timetableDate.weekDay <= 5) + break + + checkedDays++ + } + + // set the displayingDate to show in the header + if (!unified) { + if (lessons.isNotEmpty()) + displayingDate = timetableDate + profileId = profile.id + if (lessons.isEmpty()) { + views.setViewVisibility(R.id.widgetTimetableListView, View.GONE) + views.setViewVisibility(R.id.widgetTimetableNoTimetable, View.VISIBLE) + } + if (lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) { + views.setViewVisibility(R.id.widgetTimetableListView, View.GONE) + views.setViewVisibility(R.id.widgetTimetableNoLessons, View.VISIBLE) + } + } + else { + if (lessons.isEmpty()) { + val separator = ItemWidgetTimetableModel() + separator.profileId = profile.id + separator.bigStyle = widgetConfig.bigStyle + separator.darkTheme = widgetConfig.darkTheme + separator.isNoTimetableItem = true; + models.add(separator) + } + if (lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) { + val separator = ItemWidgetTimetableModel() + separator.profileId = profile.id + separator.bigStyle = widgetConfig.bigStyle + separator.darkTheme = widgetConfig.darkTheme + separator.isNoLessonsItem = true; + models.add(separator) + } + } + + // get all events for the current date + val events = app.db.eventDao().getAllByDateNow(profile.id, timetableDate)?.filterNotNull() ?: emptyList() + + lessons.forEachIndexed { pos, lesson -> + val model = ItemWidgetTimetableModel() + + model.bigStyle = widgetConfig.bigStyle + model.darkTheme = widgetConfig.darkTheme + + model.profileId = profile.id + + model.lessonId = lesson.id + model.lessonDate = timetableDate + model.startTime = lesson.displayStartTime + model.endTime = lesson.displayEndTime + + // check if the lesson has already passed or it's currently in progress + if (lesson.displayDate == today) { + lesson.displayEndTime?.let { endTime -> + model.lessonPassed = now > endTime + lesson.displayStartTime?.let { startTime -> + model.lessonCurrent = now in startTime..endTime + } + } + } + + // set where should the list view scroll to + if (model.lessonCurrent) { + scrollPos = pos + } else if (model.lessonPassed) { + scrollPos = pos + 1 + } + + // set the subject and classroom name + model.subjectName = lesson.displaySubjectName + model.classroomName = lesson.displayClassroom + + // set the bell sync to calculate progress in ListProvider + model.bellSyncDiffMillis = bellSyncDiffMillis + + // make the model aware of the lesson type + when (lesson.type) { + Lesson.TYPE_CANCELLED -> { + model.lessonCancelled = true + } + Lesson.TYPE_CHANGE, + Lesson.TYPE_SHIFTED_SOURCE, + Lesson.TYPE_SHIFTED_TARGET -> { + model.lessonChange = true + } + } + + // add every event on this lesson + for (event in events) { + if (event.startTime == null || event.startTime != lesson.displayStartTime) + continue + model.eventColors.add(if (event.type == TYPE_HOMEWORK) ItemWidgetTimetableModel.EVENT_COLOR_HOMEWORK else event.getColor()) + } + + models += model + } + } + + if (unified) { + // set the title for an unified widget + views.setTextViewText(R.id.widgetTimetableTitle, app.getString(R.string.widget_timetable_title_unified)) + views.setViewVisibility(R.id.widgetTimetableSubtitle, View.GONE) + } else { + // set the title to present the widget's profile + views.setTextViewText(R.id.widgetTimetableTitle, profileList[0].name) + views.setViewVisibility(R.id.widgetTimetableTitle, View.VISIBLE) + // make the subtitle show current date for these lessons + displayingDate?.let { + when (Date.diffDays(it, Date.getToday())) { + 0 -> views.setTextViewText(R.id.widgetTimetableSubtitle, app.getString(R.string.day_today_format, Week.getFullDayName(it.weekDay))) + 1 -> views.setTextViewText(R.id.widgetTimetableSubtitle, app.getString(R.string.day_tomorrow_format, Week.getFullDayName(it.weekDay))) + else -> views.setTextViewText(R.id.widgetTimetableSubtitle, Week.getFullDayName(it.weekDay) + " " + it.formattedString) + } + } + } + + // intent running when the header is clicked + val openIntent = Intent(app, MainActivity::class.java) + openIntent.action = "android.intent.action.MAIN" + if (!unified) { + // per-profile widget should redirect to it + correct day + profileId?.let { + openIntent.putExtra("profileId", it) + } + displayingDate?.let { + openIntent.putExtra("timetableDate", it.value) + } + } + openIntent.putExtra("fragmentId", MainActivity.DRAWER_ITEM_TIMETABLE) + val pendingOpenIntent = PendingIntent.getActivity(app, appWidgetId, openIntent, 0) + views.setOnClickPendingIntent(R.id.widgetTimetableHeader, pendingOpenIntent) + + timetables!!.put(appWidgetId, models) + + // apply the list service to the list view + val listIntent = Intent(app, WidgetTimetableService::class.java) + listIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + listIntent.data = Uri.parse(listIntent.toUri(Intent.URI_INTENT_SCHEME)) + views.setRemoteAdapter(R.id.widgetTimetableListView, listIntent) + + // create an intent used to display the lesson details dialog + val intentTemplate = Intent(app, LessonDialogActivity::class.java) + intentTemplate.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK /*or Intent.FLAG_ACTIVITY_CLEAR_TASK*/) + val pendingIntentTimetable = PendingIntent.getActivity(app, appWidgetId, intentTemplate, 0) + views.setPendingIntentTemplate(R.id.widgetTimetableListView, pendingIntentTimetable) + + if (!unified) + views.setScrollPosition(R.id.widgetTimetableListView, scrollPos) + } + + override fun onEnabled(context: Context) { + // Enter relevant functionality for when the first widget is created + } + + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + val app = context.applicationContext as App + for (appWidgetId in appWidgetIds) { + app.appConfig.widgetTimetableConfigs.remove(appWidgetId) + } + app.saveConfig("widgetTimetableConfigs") + } + + companion object { + const val ACTION_SYNC_DATA = "ACTION_SYNC_DATA" + private const val TAG = "WidgetTimetable" + + var timetables: SparseArray>? = null + + fun getPendingSelfIntent(context: Context, action: String): PendingIntent { + val intent = Intent(context, WidgetTimetable::class.java) + intent.action = action + return getPendingSelfIntent(context, intent) + } + + fun getPendingSelfIntent(context: Context, intent: Intent): PendingIntent { + return PendingIntent.getBroadcast(context, 0, intent, 0) + } + + fun drawableToBitmap(drawable: Drawable): Bitmap { + + if (drawable is BitmapDrawable) { + return drawable.bitmap + } + + val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + + return bitmap + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/AbstractConfig.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/AbstractConfig.kt new file mode 100644 index 00000000..d597a119 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/AbstractConfig.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-27. + */ + +package pl.szczodrzynski.edziennik.config + +interface AbstractConfig { + fun set(key: String, value: String?) +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt new file mode 100644 index 00000000..d96dea61 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-26. + */ + +package pl.szczodrzynski.edziennik.config + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.config.db.ConfigEntry +import pl.szczodrzynski.edziennik.config.utils.ConfigMigration +import pl.szczodrzynski.edziennik.config.utils.get +import pl.szczodrzynski.edziennik.config.utils.set +import pl.szczodrzynski.edziennik.config.utils.toHashMap +import pl.szczodrzynski.edziennik.data.db.AppDb +import kotlin.coroutines.CoroutineContext + +class Config(val db: AppDb) : CoroutineScope, AbstractConfig { + companion object { + const val DATA_VERSION = 2 + } + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Default + + val values: HashMap = hashMapOf() + + val ui by lazy { ConfigUI(this) } + val sync by lazy { ConfigSync(this) } + val timetable by lazy { ConfigTimetable(this) } + val grades by lazy { ConfigGrades(this) } + + private var mDataVersion: Int? = null + var dataVersion: Int + get() { mDataVersion = mDataVersion ?: values.get("dataVersion", 0); return mDataVersion ?: 0 } + set(value) { set("dataVersion", value); mDataVersion = value } + + private var mAppVersion: Int? = null + var appVersion: Int + get() { mAppVersion = mAppVersion ?: values.get("appVersion", BuildConfig.VERSION_CODE); return mAppVersion ?: BuildConfig.VERSION_CODE } + set(value) { set("appVersion", value); mAppVersion = value } + + private var mLoginFinished: Boolean? = null + var loginFinished: Boolean + get() { mLoginFinished = mLoginFinished ?: values.get("loginFinished", false); return mLoginFinished ?: false } + set(value) { set("loginFinished", value); mLoginFinished = value } + + private var mDevModePassword: String? = null + var devModePassword: String? + get() { mDevModePassword = mDevModePassword ?: values.get("devModePassword", null as String?); return mDevModePassword } + set(value) { set("devModePassword", value); mDevModePassword = value } + + private var mAppInstalledTime: Long? = null + var appInstalledTime: Long + get() { mAppInstalledTime = mAppInstalledTime ?: values.get("appInstalledTime", 0L); return mAppInstalledTime ?: 0L } + set(value) { set("appInstalledTime", value); mAppInstalledTime = value } + + private var mAppRateSnackbarTime: Long? = null + var appRateSnackbarTime: Long + get() { mAppRateSnackbarTime = mAppRateSnackbarTime ?: values.get("appRateSnackbarTime", 0L); return mAppRateSnackbarTime ?: 0L } + set(value) { set("appRateSnackbarTime", value); mAppRateSnackbarTime = value } + + private var mRunSync: Boolean? = null + var runSync: Boolean + get() { mRunSync = mRunSync ?: values.get("runSync", false); return mRunSync ?: false } + set(value) { set("runSync", value); mRunSync = value } + + private var rawEntries: List = db.configDao().getAllNow() + private val profileConfigs: HashMap = hashMapOf() + init { + rawEntries.toHashMap(-1, values) + } + fun migrate(app: App) { + if (dataVersion < DATA_VERSION) + ConfigMigration(app, this) + } + fun getFor(profileId: Int): ProfileConfig { + return profileConfigs[profileId] ?: ProfileConfig(db, profileId, rawEntries) + } + fun forProfile(): ProfileConfig { + return profileConfigs[App.profileId] ?: ProfileConfig(db, App.profileId, rawEntries) + } + + fun setProfile(profileId: Int) { + } + + override fun set(key: String, value: String?) { + values[key] = value + launch { + db.configDao().add(ConfigEntry(-1, key, value)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigGrades.kt new file mode 100644 index 00000000..130d8129 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigGrades.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-26. + */ + +package pl.szczodrzynski.edziennik.config + +import pl.szczodrzynski.edziennik.config.utils.get +import pl.szczodrzynski.edziennik.config.utils.set + +class ConfigGrades(private val config: Config) { + companion object { + const val ORDER_BY_DATE_DESC = 0 + const val ORDER_BY_SUBJECT_ASC = 1 + const val ORDER_BY_DATE_ASC = 2 + const val ORDER_BY_SUBJECT_DESC = 3 + } + + private var mOrderBy: Int? = null + var orderBy: Int + get() { mOrderBy = mOrderBy ?: config.values.get("gradesOrderBy", 0); return mOrderBy ?: 0 } + set(value) { config.set("gradesOrderBy", value); mOrderBy = value } +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt new file mode 100644 index 00000000..01810824 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-26. + */ + +package pl.szczodrzynski.edziennik.config + +import pl.szczodrzynski.edziennik.config.utils.get +import pl.szczodrzynski.edziennik.config.utils.getIntList +import pl.szczodrzynski.edziennik.config.utils.set + +class ConfigSync(private val config: Config) { + private var mSyncEnabled: Boolean? = null + var enabled: Boolean + get() { mSyncEnabled = mSyncEnabled ?: config.values.get("syncEnabled", true); return mSyncEnabled ?: true } + set(value) { config.set("syncEnabled", value); mSyncEnabled = value } + + private var mSyncOnlyWifi: Boolean? = null + var onlyWifi: Boolean + get() { mSyncOnlyWifi = mSyncOnlyWifi ?: config.values.get("syncOnlyWifi", false); return mSyncOnlyWifi ?: notifyAboutUpdates } + set(value) { config.set("syncOnlyWifi", value); mSyncOnlyWifi = value } + + private var mSyncInterval: Int? = null + var interval: Int + get() { mSyncInterval = mSyncInterval ?: config.values.get("syncInterval", 60*60); return mSyncInterval ?: 60*60 } + set(value) { config.set("syncInterval", value); mSyncInterval = value } + + private var mNotifyAboutUpdates: Boolean? = null + var notifyAboutUpdates: Boolean + get() { mNotifyAboutUpdates = mNotifyAboutUpdates ?: config.values.get("notifyAboutUpdates", true); return mNotifyAboutUpdates ?: true } + set(value) { config.set("notifyAboutUpdates", value); mNotifyAboutUpdates = value } + + /* ____ _ _ _ + / __ \ (_) | | | | + | | | |_ _ _ ___| |_ | |__ ___ _ _ _ __ ___ + | | | | | | | |/ _ \ __| | '_ \ / _ \| | | | '__/ __| + | |__| | |_| | | __/ |_ | | | | (_) | |_| | | \__ \ + \___\_\\__,_|_|\___|\__| |_| |_|\___/ \__,_|_| |__*/ + private var mQuietHoursStart: Long? = null + var quietHoursStart: Long + get() { mQuietHoursStart = mQuietHoursStart ?: config.values.get("quietHoursStart", 0L); return mQuietHoursStart ?: 0L } + set(value) { config.set("quietHoursStart", value); mQuietHoursStart = value } + + private var mQuietHoursEnd: Long? = null + var quietHoursEnd: Long + get() { mQuietHoursEnd = mQuietHoursEnd ?: config.values.get("quietHoursEnd", 0L); return mQuietHoursEnd ?: 0L } + set(value) { config.set("quietHoursEnd", value); mQuietHoursEnd = value } + + private var mQuietDuringLessons: Boolean? = null + var quietDuringLessons: Boolean + get() { mQuietDuringLessons = mQuietDuringLessons ?: config.values.get("quietDuringLessons", false); return mQuietDuringLessons ?: false } + set(value) { config.set("quietDuringLessons", value); mQuietDuringLessons = value } + + /* ______ _____ __ __ _______ _ + | ____/ ____| \/ | |__ __| | | + | |__ | | | \ / | | | ___ | | _____ _ __ ___ + | __|| | | |\/| | | |/ _ \| |/ / _ \ '_ \/ __| + | | | |____| | | | | | (_) | < __/ | | \__ \ + |_| \_____|_| |_| |_|\___/|_|\_\___|_| |_|__*/ + private var mTokenApp: String? = null + var tokenApp: String? + get() { mTokenApp = mTokenApp ?: config.values.get("tokenApp", null as String?); return mTokenApp } + set(value) { config.set("tokenApp", value); mTokenApp = value } + private var mTokenMobidziennik: String? = null + var tokenMobidziennik: String? + get() { mTokenMobidziennik = mTokenMobidziennik ?: config.values.get("tokenMobidziennik", null as String?); return mTokenMobidziennik } + set(value) { config.set("tokenMobidziennik", value); mTokenMobidziennik = value } + private var mTokenLibrus: String? = null + var tokenLibrus: String? + get() { mTokenLibrus = mTokenLibrus ?: config.values.get("tokenLibrus", null as String?); return mTokenLibrus } + set(value) { config.set("tokenLibrus", value); mTokenLibrus = value } + private var mTokenVulcan: String? = null + 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 mTokenMobidziennikList: List? = null + var tokenMobidziennikList: List + get() { mTokenMobidziennikList = mTokenMobidziennikList ?: config.values.getIntList("tokenMobidziennikList", listOf()); return mTokenMobidziennikList ?: listOf() } + set(value) { config.set("tokenMobidziennikList", value); mTokenMobidziennikList = value } + private var mTokenLibrusList: List? = null + var tokenLibrusList: List + get() { mTokenLibrusList = mTokenLibrusList ?: config.values.getIntList("tokenLibrusList", listOf()); return mTokenLibrusList ?: listOf() } + set(value) { config.set("tokenLibrusList", value); mTokenLibrusList = value } + private var mTokenVulcanList: List? = null + var tokenVulcanList: List + get() { mTokenVulcanList = mTokenVulcanList ?: config.values.getIntList("tokenVulcanList", listOf()); return mTokenVulcanList ?: listOf() } + set(value) { config.set("tokenVulcanList", value); mTokenVulcanList = value } +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigTimetable.kt new file mode 100644 index 00000000..2418f1d3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigTimetable.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-26. + */ + +package pl.szczodrzynski.edziennik.config + +import pl.szczodrzynski.edziennik.config.utils.get +import pl.szczodrzynski.edziennik.config.utils.set +import pl.szczodrzynski.edziennik.utils.models.Time + +class ConfigTimetable(private val config: Config) { + private var mBellSyncMultiplier: Int? = null + var bellSyncMultiplier: Int + get() { mBellSyncMultiplier = mBellSyncMultiplier ?: config.values.get("bellSyncMultiplier", 0); return mBellSyncMultiplier ?: 0 } + set(value) { config.set("bellSyncMultiplier", value); mBellSyncMultiplier = value } + + private var mBellSyncDiff: Time? = null + var bellSyncDiff: Time? + get() { mBellSyncDiff = mBellSyncDiff ?: config.values.get("bellSyncDiff", null as Time?); return mBellSyncDiff } + set(value) { config.set("bellSyncDiff", value); mBellSyncDiff = value } + + private var mCountInSeconds: Boolean? = null + var countInSeconds: Boolean + get() { mCountInSeconds = mCountInSeconds ?: config.values.get("countInSeconds", false); return mCountInSeconds ?: false } + set(value) { config.set("countInSeconds", value); mCountInSeconds = value } +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt new file mode 100644 index 00000000..2c43bb00 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-26. + */ + +package pl.szczodrzynski.edziennik.config + +import pl.szczodrzynski.edziennik.config.utils.get +import pl.szczodrzynski.edziennik.config.utils.getIntList +import pl.szczodrzynski.edziennik.config.utils.set +import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardModel + +class ConfigUI(private val config: Config) { + private var mTheme: Int? = null + var theme: Int + get() { mTheme = mTheme ?: config.values.get("theme", 1); return mTheme ?: 1 } + set(value) { config.set("theme", value); mTheme = value } + + private var mLanguage: String? = null + var language: String? + get() { mLanguage = mLanguage ?: config.values.get("language", null as String?); return mLanguage } + set(value) { config.set("language", value); mLanguage = value } + + private var mHeaderBackground: String? = null + var headerBackground: String? + get() { mHeaderBackground = mHeaderBackground ?: config.values.get("headerBg", null as String?); return mHeaderBackground } + set(value) { config.set("headerBg", value); mHeaderBackground = value } + + private var mAppBackground: String? = null + var appBackground: String? + get() { mAppBackground = mAppBackground ?: config.values.get("appBg", null as String?); return mAppBackground } + set(value) { config.set("appBg", value); mAppBackground = value } + + private var mMiniMenuVisible: Boolean? = null + var miniMenuVisible: Boolean + get() { mMiniMenuVisible = mMiniMenuVisible ?: config.values.get("miniMenuVisible", false); return mMiniMenuVisible ?: false } + set(value) { config.set("miniMenuVisible", value); mMiniMenuVisible = value } + + private var mMiniMenuButtons: List? = null + var miniMenuButtons: List + get() { mMiniMenuButtons = mMiniMenuButtons ?: config.values.getIntList("miniMenuButtons", listOf()); return mMiniMenuButtons ?: listOf() } + set(value) { config.set("miniMenuButtons", value); mMiniMenuButtons = value } + + private var mOpenDrawerOnBackPressed: Boolean? = null + var openDrawerOnBackPressed: Boolean + get() { mOpenDrawerOnBackPressed = mOpenDrawerOnBackPressed ?: config.values.get("openDrawerOnBackPressed", false); return mOpenDrawerOnBackPressed ?: false } + set(value) { config.set("openDrawerOnBackPressed", value); mOpenDrawerOnBackPressed = value } + + private var mAgendaViewType: Int? = null + var agendaViewType: Int + get() { mAgendaViewType = mAgendaViewType ?: config.values.get("agendaViewType", 0); return mAgendaViewType ?: 0 } + set(value) { config.set("agendaViewType", value); mAgendaViewType = value } + + private var mHomeCards: List? = null + var homeCards: List + get() { mHomeCards = mHomeCards ?: config.values.get("homeCards", listOf(), HomeCardModel::class.java); return mHomeCards ?: listOf() } + set(value) { config.set("homeCards", value); mHomeCards = value } + + private var mSnowfall: Boolean? = null + var snowfall: Boolean + get() { mSnowfall = mSnowfall ?: config.values.get("snowfall", false); return mSnowfall ?: false } + set(value) { config.set("snowfall", value); mSnowfall = value } + + private var mBottomSheetOpened: Boolean? = null + var bottomSheetOpened: Boolean + get() { mBottomSheetOpened = mBottomSheetOpened ?: config.values.get("bottomSheetOpened", false); return mBottomSheetOpened ?: false } + set(value) { config.set("bottomSheetOpened", value); mBottomSheetOpened = value } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt new file mode 100644 index 00000000..ec5ae3f0 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-27. + */ + +package pl.szczodrzynski.edziennik.config + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import pl.szczodrzynski.edziennik.config.db.ConfigEntry +import pl.szczodrzynski.edziennik.config.utils.get +import pl.szczodrzynski.edziennik.config.utils.set +import pl.szczodrzynski.edziennik.config.utils.toHashMap +import pl.szczodrzynski.edziennik.data.db.AppDb +import kotlin.coroutines.CoroutineContext + +class ProfileConfig(val db: AppDb, val profileId: Int, rawEntries: List) : CoroutineScope, AbstractConfig { + companion object { + const val DATA_VERSION = 1 + } + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Default + + val values: HashMap = hashMapOf() + + val grades by lazy { ProfileConfigGrades(this) } + /* + val sync by lazy { ConfigSync(this) } + val timetable by lazy { ConfigTimetable(this) } + val grades by lazy { ConfigGrades(this) }*/ + + private var mDataVersion: Int? = null + var dataVersion: Int + get() { mDataVersion = mDataVersion ?: values.get("dataVersion", 0); return mDataVersion ?: 0 } + set(value) { set("dataVersion", value); mDataVersion = value } + + init { + rawEntries.toHashMap(profileId, values) + /*if (dataVersion < DATA_VERSION) + ProfileConfigMigration(this)*/ + } + + override fun set(key: String, value: String?) { + values[key] = value + launch { + db.configDao().add(ConfigEntry(profileId, key, value)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigGrades.kt new file mode 100644 index 00000000..e90d9be3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigGrades.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-27. + */ + +package pl.szczodrzynski.edziennik.config + +import pl.szczodrzynski.edziennik.config.utils.get +import pl.szczodrzynski.edziennik.config.utils.set +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile.Companion.COLOR_MODE_WEIGHTED +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile.Companion.YEAR_ALL_GRADES + +class ProfileConfigGrades(private val config: ProfileConfig) { + private var mColorMode: Int? = null + var colorMode: Int + get() { mColorMode = mColorMode ?: config.values.get("gradesColorMode", COLOR_MODE_WEIGHTED); return mColorMode ?: COLOR_MODE_WEIGHTED } + set(value) { config.set("gradesColorMode", value); mColorMode = value } + + private var mYearAverageMode: Int? = null + var yearAverageMode: Int + get() { mYearAverageMode = mYearAverageMode ?: config.values.get("yearAverageMode", YEAR_ALL_GRADES); return mYearAverageMode ?: YEAR_ALL_GRADES } + set(value) { config.set("yearAverageMode", value); mYearAverageMode = value } + + private var mCountZeroToAvg: Boolean? = null + var countZeroToAvg: Boolean + get() { mCountZeroToAvg = mCountZeroToAvg ?: config.values.get("countZeroToAvg", true); return mCountZeroToAvg ?: true } + set(value) { config.set("countZeroToAvg", value); mCountZeroToAvg = value } +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/db/ConfigDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/db/ConfigDao.kt new file mode 100644 index 00000000..d4665910 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/db/ConfigDao.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-27. + */ + +package pl.szczodrzynski.edziennik.config.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface ConfigDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun add(entry: ConfigEntry) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun addAll(list: List) + + @Query("SELECT * FROM config WHERE profileId = -1") + fun getAllNow(): List + + @Query("SELECT * FROM config WHERE profileId = :profileId") + fun getAllNow(profileId: Int): List +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/db/ConfigEntry.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/db/ConfigEntry.kt new file mode 100644 index 00000000..6da441fe --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/db/ConfigEntry.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-26. + */ + +package pl.szczodrzynski.edziennik.config.db + +import androidx.room.Entity + +@Entity(tableName = "config", primaryKeys = ["profileId", "key"]) +data class ConfigEntry( + val profileId: Int = -1, + val key: String, + val value: String? +) \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigExtensions.kt new file mode 100644 index 00000000..aec5ebee --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigExtensions.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-27. + */ + +package pl.szczodrzynski.edziennik.config.utils + +import com.google.gson.* +import com.google.gson.reflect.TypeToken +import pl.szczodrzynski.edziennik.config.AbstractConfig +import pl.szczodrzynski.edziennik.config.db.ConfigEntry +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +private val gson = Gson() + +fun AbstractConfig.set(key: String, value: Int) { + set(key, value.toString()) +} +fun AbstractConfig.set(key: String, value: Boolean) { + set(key, value.toString()) +} +fun AbstractConfig.set(key: String, value: Long) { + set(key, value.toString()) +} +fun AbstractConfig.set(key: String, value: Float) { + set(key, value.toString()) +} +fun AbstractConfig.set(key: String, value: Date?) { + set(key, value?.stringY_m_d) +} +fun AbstractConfig.set(key: String, value: Time?) { + set(key, value?.stringValue) +} +fun AbstractConfig.set(key: String, value: JsonElement?) { + set(key, value?.toString()) +} +fun AbstractConfig.set(key: String, value: List?) { + set(key, value?.let { gson.toJson(it) }) +} +fun AbstractConfig.setStringList(key: String, value: List?) { + set(key, value?.let { gson.toJson(it) }) +} +fun AbstractConfig.setIntList(key: String, value: List?) { + set(key, value?.let { gson.toJson(it) }) +} +fun AbstractConfig.setLongList(key: String, value: List?) { + set(key, value?.let { gson.toJson(it) }) +} + +fun HashMap.get(key: String, default: String?): String? { + return this[key] ?: default +} +fun HashMap.get(key: String, default: Boolean): Boolean { + return this[key]?.toBoolean() ?: default +} +fun HashMap.get(key: String, default: Int): Int { + return this[key]?.toIntOrNull() ?: default +} +fun HashMap.get(key: String, default: Long): Long { + return this[key]?.toLongOrNull() ?: default +} +fun HashMap.get(key: String, default: Float): Float { + return this[key]?.toFloatOrNull() ?: default +} +fun HashMap.get(key: String, default: Date?): Date? { + return this[key]?.let { Date.fromY_m_d(it) } ?: default +} +fun HashMap.get(key: String, default: Time?): Time? { + return this[key]?.let { Time.fromHms(it) } ?: default +} +fun HashMap.get(key: String, default: JsonObject?): JsonObject? { + return this[key]?.let { JsonParser().parse(it)?.asJsonObject } ?: default +} +fun HashMap.get(key: String, default: JsonArray?): JsonArray? { + return this[key]?.let { JsonParser().parse(it)?.asJsonArray } ?: default +} +/* !!! cannot use mutable list here - modifying it will not update the DB */ +fun HashMap.get(key: String, default: List?, classOfT: Class): List? { + return this[key]?.let { ConfigGsonUtils().deserializeList(gson, it, classOfT) } ?: default +} +fun HashMap.getStringList(key: String, default: List?): List? { + return this[key]?.let { gson.fromJson>(it, object: TypeToken>(){}.type) } ?: default +} +fun HashMap.getIntList(key: String, default: List?): List? { + return this[key]?.let { gson.fromJson>(it, object: TypeToken>(){}.type) } ?: default +} +fun HashMap.getLongList(key: String, default: List?): List? { + return this[key]?.let { gson.fromJson>(it, object: TypeToken>(){}.type) } ?: default +} + +fun List.toHashMap(profileId: Int, map: HashMap) { + map.clear() + forEach { + if (it.profileId == profileId) + map[it.key] = it.value + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigGsonUtils.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigGsonUtils.kt new file mode 100644 index 00000000..eb04578d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigGsonUtils.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-2. + */ +package pl.szczodrzynski.edziennik.config.utils + +import com.google.gson.Gson +import com.google.gson.JsonParser +import pl.szczodrzynski.edziennik.getInt +import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardModel +import pl.szczodrzynski.edziennik.utils.models.Time + +class ConfigGsonUtils { + fun deserializeList(gson: Gson, str: String?, classOfT: Class): List { + val json = JsonParser().parse(str) + val list: MutableList = mutableListOf() + if (!json.isJsonArray) + return list + + json.asJsonArray.forEach { e -> + when (classOfT) { + String::class.java -> { + list += e.asString as T + } + HomeCardModel::class.java -> { + val o = e.asJsonObject + list += HomeCardModel( + o.getInt("profileId", 0), + o.getInt("cardId", 0) + ) as T + } + Time::class.java -> { + val o = e.asJsonObject + list += Time( + o.getInt("hour", 0), + o.getInt("minute", 0), + o.getInt("second", 0) + ) as T + } + } + } + + return list + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigMigration.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigMigration.kt new file mode 100644 index 00000000..4ed95f6f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigMigration.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-27. + */ + +package pl.szczodrzynski.edziennik.config.utils + +import android.content.Context +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS +import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK +import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_VULCAN +import pl.szczodrzynski.edziennik.config.Config +import pl.szczodrzynski.edziennik.utils.models.Time + +class ConfigMigration(app: App, config: Config) { + init { config.apply { + val p = app.getSharedPreferences("pl.szczodrzynski.edziennik_profiles", Context.MODE_PRIVATE) + val s = "app.appConfig" + + if (dataVersion < 1) { + ui.theme = p.getString("$s.appTheme", null)?.toIntOrNull() ?: 1 + sync.enabled = p.getString("$s.registerSyncEnabled", null)?.toBoolean() ?: true + sync.interval = p.getString("$s.registerSyncEnabled", null)?.toIntOrNull() ?: 3600 + val oldButtons = p.getString("$s.miniDrawerButtonIds", null)?.let { str -> + str.replace("[\\[\\]]*".toRegex(), "") + .split(",\\s?".toRegex()) + .mapNotNull { it.toIntOrNull() } + } + ui.miniMenuButtons = oldButtons ?: listOf( + MainActivity.DRAWER_ITEM_HOME, + MainActivity.DRAWER_ITEM_TIMETABLE, + MainActivity.DRAWER_ITEM_AGENDA, + MainActivity.DRAWER_ITEM_GRADES, + MainActivity.DRAWER_ITEM_MESSAGES, + MainActivity.DRAWER_ITEM_HOMEWORK, + MainActivity.DRAWER_ITEM_SETTINGS + ) + dataVersion = 1 + } + if (dataVersion < 2) { + devModePassword = p.getString("$s.devModePassword", null).fix() + sync.tokenApp = p.getString("$s.fcmToken", null).fix() + timetable.bellSyncMultiplier = p.getString("$s.bellSyncMultiplier", null)?.toIntOrNull() ?: 0 + sync.quietHoursStart = p.getString("$s.quietHoursStart", null)?.toLongOrNull() ?: 0 + appRateSnackbarTime = p.getString("$s.appRateSnackbarTime", null)?.toLongOrNull() ?: 0 + sync.quietHoursEnd = p.getString("$s.quietHoursEnd", null)?.toLongOrNull() ?: 0 + timetable.countInSeconds = p.getString("$s.countInSeconds", null)?.toBoolean() ?: false + ui.headerBackground = p.getString("$s.headerBackground", null).fix() + ui.appBackground = p.getString("$s.appBackground", null).fix() + ui.language = p.getString("$s.language", null).fix() + appVersion = p.getString("$s.lastAppVersion", null)?.toIntOrNull() ?: BuildConfig.VERSION_CODE + appInstalledTime = p.getString("$s.appInstalledTime", null)?.toLongOrNull() ?: 0 + grades.orderBy = p.getString("$s.gradesOrderBy", null)?.toIntOrNull() ?: 0 + sync.quietDuringLessons = p.getString("$s.quietDuringLessons", null)?.toBoolean() ?: false + ui.miniMenuVisible = p.getString("$s.miniDrawerVisible", null)?.toBoolean() ?: false + loginFinished = p.getString("$s.loginFinished", null)?.toBoolean() ?: false + sync.onlyWifi = p.getString("$s.registerSyncOnlyWifi", null)?.toBoolean() ?: false + sync.notifyAboutUpdates = p.getString("$s.notifyAboutUpdates", null)?.toBoolean() ?: true + timetable.bellSyncDiff = p.getString("$s.bellSyncDiff", null)?.let { Gson().fromJson(it, Time::class.java) } + + sync.tokenMobidziennikList = listOf() + sync.tokenVulcanList = listOf() + sync.tokenLibrusList = listOf() + val tokens = p.getString("$s.fcmTokens", null)?.let { Gson().fromJson>>>(it, object: TypeToken>>>(){}.type) } + tokens?.forEach { + val token = it.value.first + when (it.key) { + LOGIN_TYPE_MOBIDZIENNIK -> sync.tokenMobidziennik = token + LOGIN_TYPE_VULCAN -> sync.tokenVulcan = token + LOGIN_TYPE_LIBRUS -> sync.tokenLibrus = token + } + } + dataVersion = 2 + } + }} + + private fun String?.fix(): String? { + return this?.replace("\"", "")?.let { if (it == "null") null else it } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt new file mode 100644 index 00000000..31c4dbc3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-1. + */ + +package pl.szczodrzynski.edziennik.config.utils + +import android.content.Context +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.config.Config + +class ProfileConfigMigration(app: App, config: Config) { + init { config.apply { + val p = app.getSharedPreferences("pl.szczodrzynski.edziennik_profiles", Context.MODE_PRIVATE) + val s = "app.appConfig" + + if (dataVersion < 1) { + + //dataVersion = 1 + } + if (dataVersion < 2) { + //gradesColorMode do profilu ! + //agendaViewType do profilu ! + // app.appConfig.dontCountZeroToAverage do profilu ! + } + }} +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt new file mode 100644 index 00000000..b14e8e7b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt @@ -0,0 +1,307 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-28. + */ + +package pl.szczodrzynski.edziennik.data.api + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.IBinder +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.events.* +import pl.szczodrzynski.edziennik.data.api.events.requests.ServiceCloseRequest +import pl.szczodrzynski.edziennik.data.api.events.requests.TaskCancelRequest +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.task.* +import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull +import pl.szczodrzynski.edziennik.utils.Utils.d +import kotlin.math.min +import kotlin.math.roundToInt + +class ApiService : Service() { + companion object { + const val TAG = "ApiService" + const val NOTIFICATION_API_CHANNEL_ID = "pl.szczodrzynski.edziennik.GET_DATA" + fun start(context: Context) { + context.startService(Intent(context, ApiService::class.java)) + } + fun startAndRequest(context: Context, request: Any) { + context.startService(Intent(context, ApiService::class.java)) + EventBus.getDefault().postSticky(request) + } + } + + private val app by lazy { applicationContext as App } + + private val syncingProfiles = mutableListOf() + + private val finishingTaskQueue = mutableListOf( + SzkolnyTask.sync(syncingProfiles), + NotifyTask() + ) + private val allTaskList = mutableListOf() + private val taskQueue = mutableListOf() + private val errorList = mutableListOf() + + private var serviceClosed = false + private var taskCancelled = false + private var taskIsRunning = false + private var taskRunning: IApiTask? = null // for debug purposes + private var taskRunningId = -1 + private var taskMaximumId = 0 + + private var taskProfileId = -1 + private var taskProgress = -1f + private var taskProgressText: String? = null + + private val notification by lazy { EdziennikNotification(this) } + + private var lastEventTime = System.currentTimeMillis() + private var taskCancelTries = 0 + + /* ______ _ _ _ _ _____ _ _ _ _ + | ____| | | (_) (_) | / ____| | | | | | | + | |__ __| |_____ ___ _ __ _ __ _| | __ | | __ _| | | |__ __ _ ___| | __ + | __| / _` |_ / |/ _ \ '_ \| '_ \| | |/ / | | / _` | | | '_ \ / _` |/ __| |/ / + | |___| (_| |/ /| | __/ | | | | | | | < | |___| (_| | | | |_) | (_| | (__| < + |______\__,_/___|_|\___|_| |_|_| |_|_|_|\_\ \_____\__,_|_|_|_.__/ \__,_|\___|_|\*/ + private val taskCallback = object : EdziennikCallback { + override fun onCompleted() { + lastEventTime = System.currentTimeMillis() + d(TAG, "Task $taskRunningId (profile $taskProfileId) - $taskProgressText - finished") + EventBus.getDefault().postSticky(ApiTaskFinishedEvent(taskProfileId)) + clearTask() + + notification.setIdle().post() + runTask() + } + + override fun onError(apiError: ApiError) { + lastEventTime = System.currentTimeMillis() + d(TAG, "Task $taskRunningId threw an error - $apiError") + apiError.profileId = taskProfileId + EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError)) + errorList.add(apiError) + apiError.throwable?.printStackTrace() + if (apiError.isCritical) { + taskRunning?.cancel() + notification.setCriticalError().post() + clearTask() + runTask() + } + else { + notification.addError().post() + } + } + + override fun onProgress(step: Float) { + lastEventTime = System.currentTimeMillis() + if (step <= 0) + return + if (taskProgress < 0) + taskProgress = 0f + taskProgress += step + taskProgress = min(100f, taskProgress) + d(TAG, "Task $taskRunningId progress: ${taskProgress.roundToInt()}%") + EventBus.getDefault().post(ApiTaskProgressEvent(taskProfileId, taskProgress, taskProgressText)) + notification.setProgress(taskProgress).post() + } + + override fun onStartProgress(stringRes: Int) { + lastEventTime = System.currentTimeMillis() + taskProgressText = getString(stringRes) + d(TAG, "Task $taskRunningId progress: $taskProgressText") + EventBus.getDefault().post(ApiTaskProgressEvent(taskProfileId, taskProgress, taskProgressText)) + notification.setProgressText(taskProgressText).post() + } + } + + /* _______ _ _ _ + |__ __| | | | | (_) + | | __ _ ___| | __ _____ _____ ___ _ _| |_ _ ___ _ __ + | |/ _` / __| |/ / / _ \ \/ / _ \/ __| | | | __| |/ _ \| '_ \ + | | (_| \__ \ < | __/> < __/ (__| |_| | |_| | (_) | | | | + |_|\__,_|___/_|\_\ \___/_/\_\___|\___|\__,_|\__|_|\___/|_| |*/ + private fun runTask() { + checkIfTaskFrozen() + if (taskIsRunning) + return + if (taskCancelled || serviceClosed || (taskQueue.isEmpty() && finishingTaskQueue.isEmpty())) { + serviceClosed = false + allCompleted() + return + } + + lastEventTime = System.currentTimeMillis() + + val task = if (taskQueue.isEmpty()) finishingTaskQueue.removeAt(0) else taskQueue.removeAt(0) + task.taskId = ++taskMaximumId + task.prepare(app) + taskIsRunning = true + taskRunningId = task.taskId + taskRunning = task + taskProfileId = task.profileId + taskProgress = -1f + taskProgressText = task.taskName + + d(TAG, "Executing task $taskRunningId ($taskProgressText) - $task") + + // update the notification + notification.setCurrentTask(taskRunningId, taskProgressText).post() + + // post an event + EventBus.getDefault().post(ApiTaskStartedEvent(taskProfileId, task.profile)) + + task.profile?.let { syncingProfiles.add(it) } + + try { + when (task) { + is EdziennikTask -> task.run(app, taskCallback) + is NotifyTask -> task.run(app, taskCallback) + is ErrorReportTask -> task.run(app, taskCallback, notification, errorList) + is SzkolnyTask -> task.run(app, taskCallback) + } + } catch (e: Exception) { + taskCallback.onError(ApiError(TAG, EXCEPTION_API_TASK).withThrowable(e)) + } + } + + /** + * Check if a task is inactive for more than 30 seconds. + * If the user tries to cancel a task with no success at least three times, + * consider it frozen as well. + * + * This usually means it is broken and won't become active again. + * This method cancels the task and removes any pointers to it. + */ + private fun checkIfTaskFrozen(): Boolean { + if (System.currentTimeMillis() - lastEventTime > 30*1000 + || taskCancelTries >= 3) { + val time = System.currentTimeMillis() - lastEventTime + d(TAG, "!!! Task $taskRunningId froze for $time ms. $taskRunning") + clearTask() + return true + } + return false + } + + /** + * Stops the service if the current task is frozen/broken. + */ + private fun stopIfTaskFrozen() { + if (checkIfTaskFrozen()) { + allCompleted() + } + } + + /** + * Remove any task descriptors or pointers from the service. + */ + private fun clearTask() { + taskIsRunning = false + taskRunningId = -1 + taskRunning = null + taskProfileId = -1 + taskProgress = -1f + taskProgressText = null + taskCancelled = false + taskCancelTries = 0 + } + + private fun allCompleted() { + EventBus.getDefault().postSticky(ApiTaskAllFinishedEvent()) + stopSelf() + } + + /* ______ _ ____ + | ____| | | | _ \ + | |____ _____ _ __ | |_| |_) |_ _ ___ + | __\ \ / / _ \ '_ \| __| _ <| | | / __| + | |___\ V / __/ | | | |_| |_) | |_| \__ \ + |______\_/ \___|_| |_|\__|____/ \__,_|__*/ + @Subscribe(sticky = true, threadMode = ThreadMode.ASYNC) + fun onApiTask(task: IApiTask) { + EventBus.getDefault().removeStickyEvent(task) + d(TAG, task.toString()) + + // fix for duplicated tasks, thank you EventBus + if (task in allTaskList) + return + allTaskList += task + + if (task is EdziennikTask) { + when (task.request) { + is EdziennikTask.SyncRequest -> app.db.profileDao().idsForSyncNow.forEach { + taskQueue += EdziennikTask.syncProfile(it) + } + is EdziennikTask.SyncProfileListRequest -> task.request.profileList.forEach { + taskQueue += EdziennikTask.syncProfile(it) + } + else -> { + taskQueue += task + } + } + } + else { + taskQueue += task + } + d(TAG, "EventBus received an IApiTask: $task") + d(TAG, "Current queue:") + taskQueue.forEach { + d(TAG, " - $it") + } + runTask() + } + + @Subscribe(sticky = true, threadMode = ThreadMode.ASYNC) + fun onTaskCancelRequest(request: TaskCancelRequest) { + EventBus.getDefault().removeStickyEvent(request) + d(TAG, request.toString()) + + taskCancelTries++ + taskCancelled = true + taskRunning?.cancel() + stopIfTaskFrozen() + } + @Subscribe(sticky = true, threadMode = ThreadMode.ASYNC) + fun onServiceCloseRequest(request: ServiceCloseRequest) { + EventBus.getDefault().removeStickyEvent(request) + d(TAG, request.toString()) + + serviceClosed = true + taskCancelled = true + taskRunning?.cancel() + allCompleted() + } + + /* _____ _ _ _ + / ____| (_) (_) | | + | (___ ___ _ ____ ___ ___ ___ _____ _____ _ __ _ __ _ __| | ___ ___ + \___ \ / _ \ '__\ \ / / |/ __/ _ \ / _ \ \ / / _ \ '__| '__| |/ _` |/ _ \/ __| + ____) | __/ | \ V /| | (_| __/ | (_) \ V / __/ | | | | | (_| | __/\__ \ + |_____/ \___|_| \_/ |_|\___\___| \___/ \_/ \___|_| |_| |_|\__,_|\___||__*/ + override fun onCreate() { + d(TAG, "Service created") + EventBus.getDefault().register(this) + notification.setIdle().setCloseAction() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + d(TAG, "Foreground service onStartCommand") + startForeground(EdziennikNotification.NOTIFICATION_ID, notification.notification) + return START_NOT_STICKY + } + + override fun onDestroy() { + EventBus.getDefault().unregister(this) + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/AppError.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/AppError.java deleted file mode 100644 index ea72e969..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/AppError.java +++ /dev/null @@ -1,321 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api; - -import android.content.Context; -import android.os.AsyncTask; -import android.os.Build; -import android.util.Log; - -import com.google.gson.JsonObject; - -import java.io.InterruptedIOException; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; - -import javax.net.ssl.SSLException; - -import im.wangchao.mhttp.Request; -import im.wangchao.mhttp.Response; -import pl.szczodrzynski.edziennik.R; - - -public class AppError { - public static final int CODE_OTHER = 0; - public static final int CODE_OK = 1; - public static final int CODE_NO_INTERNET = 10; - public static final int CODE_SSL_ERROR = 13; - public static final int CODE_ARCHIVED = 5; - public static final int CODE_MAINTENANCE = 6; - public static final int CODE_LOGIN_ERROR = 7; - public static final int CODE_ACCOUNT_MISMATCH = 8; - public static final int CODE_APP_SERVER_ERROR = 9; - public static final int CODE_MULTIACCOUNT_SETUP = 12; - public static final int CODE_TIMEOUT = 11; - public static final int CODE_PROFILE_NOT_FOUND = 14; - public static final int CODE_ATTACHMENT_NOT_AVAILABLE = 28; - // user's fault - public static final int CODE_INVALID_LOGIN = 2; - public static final int CODE_INVALID_SERVER_ADDRESS = 21; - public static final int CODE_INVALID_SCHOOL_NAME = 22; - public static final int CODE_INVALID_DEVICE = 23; - public static final int CODE_OLD_PASSWORD = 4; - public static final int CODE_INVALID_TOKEN = 24; - public static final int CODE_EXPIRED_TOKEN = 27; - public static final int CODE_INVALID_SYMBOL = 25; - public static final int CODE_INVALID_PIN = 26; - public static final int CODE_LIBRUS_NOT_ACTIVATED = 29; - public static final int CODE_SYNERGIA_NOT_ACTIVATED = 32; - public static final int CODE_LIBRUS_DISCONNECTED = 31; - public static final int CODE_PROFILE_ARCHIVED = 30; - - public static final int CODE_INTERNAL_MISSING_DATA = 100; - // internal errors - not for user's information. - // these error codes are processed in API main classes - public static final int CODE_INTERNAL_LIBRUS_ACCOUNT_410 = 120; - public static final int CODE_INTERNAL_LIBRUS_ACCOUNT_410_ = 120; - - public String TAG; - public int line; - public int errorCode; - public String errorText; - public Response response; - public Request request; - public Throwable throwable; - public String apiResponse; - - public AppError(String TAG, int line, int errorCode, String errorText, Response response, Request request, Throwable throwable, String apiResponse) { - this.TAG = TAG; - this.line = line; - this.errorCode = errorCode; - this.errorText = errorText; - this.response = response; - this.request = request; - this.throwable = throwable; - this.apiResponse = apiResponse; - } - - public AppError(String TAG, int line, int errorCode) { - this(TAG, line, errorCode, null, null, null, null, null); - } - public AppError(String TAG, int line, int errorCode, Response response, Throwable throwable) { - this(TAG, line, errorCode, null, response, response == null ? null : response.request(), throwable, null); - } - public AppError(String TAG, int line, int errorCode, Response response) { - this(TAG, line, errorCode, null, response, response == null ? null : response.request(), null, null); - } - public AppError(String TAG, int line, int errorCode, Throwable throwable, String apiResponse) { - this(TAG, line, errorCode, null, null, null, throwable, apiResponse); - } - public AppError(String TAG, int line, int errorCode, Throwable throwable, JsonObject apiResponse) { - this(TAG, line, errorCode, null, null, null, throwable, apiResponse.toString()); - } - public AppError(String TAG, int line, int errorCode, String errorText, Response response, JsonObject apiResponse) { - this(TAG, line, errorCode, errorText, response, response == null ? null : response.request(), null, apiResponse.toString()); - } - public AppError(String TAG, int line, int errorCode, String errorText, Response response, String apiResponse) { - this(TAG, line, errorCode, errorText, response, response == null ? null : response.request(), null, apiResponse); - } - public AppError(String TAG, int line, int errorCode, String errorText, String apiResponse) { - this(TAG, line, errorCode, errorText, null, null, null, apiResponse); - } - public AppError(String TAG, int line, int errorCode, String errorText, JsonObject apiResponse) { - this(TAG, line, errorCode, errorText, null, null, null, apiResponse.toString()); - } - public AppError(String TAG, int line, int errorCode, String errorText) { - this(TAG, line, errorCode, errorText, null, null, null, null); - } - public AppError(String TAG, int line, int errorCode, JsonObject apiResponse) { - this(TAG, line, errorCode, null, null, null, null, apiResponse.toString()); - } - public AppError(String TAG, int line, int errorCode, Response response, Throwable throwable, JsonObject apiResponse) { - this(TAG, line, errorCode, null, response, response == null ? null : response.request(), throwable, apiResponse.toString()); - } - public AppError(String TAG, int line, int errorCode, Response response, Throwable throwable, String apiResponse) { - this(TAG, line, errorCode, null, response, response == null ? null : response.request(), throwable, apiResponse); - } - public AppError(String TAG, int line, int errorCode, Response response, String apiResponse) { - this(TAG, line, errorCode, null, response, response == null ? null : response.request(), null, apiResponse); - } - public AppError(String TAG, int line, int errorCode, Response response, JsonObject apiResponse) { - this(TAG, line, errorCode, null, response, response == null ? null : response.request(), null, apiResponse.toString()); - } - - public String getDetails(Context context) { - StringBuilder sb = new StringBuilder(); - sb.append(stringErrorCode(context, errorCode, errorText)).append("\n"); - sb.append("(").append(stringErrorType(errorCode)).append("#").append(errorCode).append(")\n"); - sb.append("at ").append(TAG).append(":").append(line).append("\n"); - sb.append("\n"); - if (throwable == null) - sb.append("Throwable is null"); - else - sb.append(Log.getStackTraceString(throwable)); - sb.append("\n"); - sb.append(Build.MANUFACTURER).append(" ").append(Build.BRAND).append(" ").append(Build.MODEL).append(" ").append(Build.DEVICE).append("\n"); - - return sb.toString(); - } - - public interface GetApiResponseCallback { - void onSuccess(String apiResponse); - } - /** - * - * @param context a Context - * @param apiResponseCallback a callback executed on a worker thread - */ - public void getApiResponse(Context context, GetApiResponseCallback apiResponseCallback) { - StringBuilder sb = new StringBuilder(); - sb.append("Request:\n"); - if (request != null) { - sb.append(request.method()).append(" ").append(request.url().toString()).append("\n"); - sb.append(request.headers().toString()).append("\n"); - sb.append("\n"); - sb.append(request.bodyToString()).append("\n\n"); - } - else - sb.append("null\n\n"); - - if (apiResponse == null && response != null) - apiResponse = response.parserErrorBody; - - sb.append("Response:\n"); - if (response != null) { - sb.append(response.code()).append(" ").append(response.message()).append("\n"); - sb.append(response.headers().toString()).append("\n"); - sb.append("\n"); - if (apiResponse == null) { - if (Thread.currentThread().getName().equals("main")) { - AsyncTask.execute(() -> { - if (response.raw().body() != null) { - try { - sb.append(response.raw().body().string()); - } catch (Exception e) { - sb.append("Exception while getting response body:\n").append(Log.getStackTraceString(e)); - } - } - else { - sb.append("null"); - } - apiResponseCallback.onSuccess(sb.toString()); - }); - } - else { - if (response.raw().body() != null) { - try { - sb.append(response.raw().body().string()); - } catch (Exception e) { - sb.append("Exception while getting response body:\n").append(Log.getStackTraceString(e)); - } - } - else { - sb.append("null"); - } - apiResponseCallback.onSuccess(sb.toString()); - } - return; - } - } - else - sb.append("null\n\n"); - - sb.append("API Response:\n"); - if (apiResponse != null) { - sb.append(apiResponse).append("\n\n"); - } - else { - sb.append("null\n\n"); - } - - apiResponseCallback.onSuccess(sb.toString()); - } - - public AppError changeIfCodeOther() { - if (errorCode != CODE_OTHER && errorCode != CODE_MAINTENANCE) - return this; - if (throwable instanceof UnknownHostException) - errorCode = CODE_NO_INTERNET; - else if (throwable instanceof SSLException) - errorCode = CODE_SSL_ERROR; - else if (throwable instanceof SocketTimeoutException) - errorCode = CODE_TIMEOUT; - else if (throwable instanceof InterruptedIOException) - errorCode = CODE_NO_INTERNET; - else if (response != null && - (response.code() == 424 - || response.code() == 400 - || response.code() == 401 - || response.code() == 500 - || response.code() == 503 - || response.code() == 404)) - errorCode = CODE_MAINTENANCE; - return this; - } - - public String asReadableString(Context context) { - return stringErrorCode(context, errorCode, errorText) + (errorCode == CODE_MAINTENANCE && errorText != null && !errorText.isEmpty() ? " ("+errorText+")" : ""); - } - - public static String stringErrorCode(Context context, int errorCode, String errorText) - { - switch (errorCode) { - case CODE_OK: - return context.getString(R.string.sync_error_ok); - case CODE_INVALID_LOGIN: - return context.getString(R.string.sync_error_invalid_login); - case CODE_LOGIN_ERROR: - return context.getString(R.string.sync_error_login_error); - case CODE_INVALID_DEVICE: - return context.getString(R.string.sync_error_invalid_device); - case CODE_OLD_PASSWORD: - return context.getString(R.string.sync_error_old_password); - case CODE_ARCHIVED: - return context.getString(R.string.sync_error_archived); - case CODE_MAINTENANCE: - return context.getString(R.string.sync_error_maintenance); - case CODE_NO_INTERNET: - return context.getString(R.string.sync_error_no_internet); - case CODE_ACCOUNT_MISMATCH: - return context.getString(R.string.sync_error_account_mismatch); - case CODE_APP_SERVER_ERROR: - return context.getString(R.string.sync_error_app_server); - case CODE_TIMEOUT: - return context.getString(R.string.sync_error_timeout); - case CODE_SSL_ERROR: - return context.getString(R.string.sync_error_ssl); - case CODE_INVALID_SERVER_ADDRESS: - return context.getString(R.string.sync_error_invalid_server_address); - case CODE_INVALID_SCHOOL_NAME: - return context.getString(R.string.sync_error_invalid_school_name); - case CODE_PROFILE_NOT_FOUND: - return context.getString(R.string.sync_error_profile_not_found); - case CODE_INVALID_TOKEN: - return context.getString(R.string.sync_error_invalid_token); - case CODE_ATTACHMENT_NOT_AVAILABLE: - return context.getString(R.string.sync_error_attachment_not_available); - case CODE_LIBRUS_NOT_ACTIVATED: - return context.getString(R.string.sync_error_librus_not_activated); - case CODE_PROFILE_ARCHIVED: - return context.getString(R.string.sync_error_profile_archived); - case CODE_LIBRUS_DISCONNECTED: - return context.getString(R.string.sync_error_librus_disconnected); - case CODE_SYNERGIA_NOT_ACTIVATED: - return context.getString(R.string.sync_error_synergia_not_activated); - default: - case CODE_MULTIACCOUNT_SETUP: - case CODE_OTHER: - return errorText != null ? errorText : context.getString(R.string.sync_error_unknown); - } - } - public static String stringErrorType(int errorCode) - { - switch (errorCode) { - default: - case CODE_OTHER: return "CODE_OTHER"; - case CODE_OK: return "CODE_OK"; - case CODE_NO_INTERNET: return "CODE_NO_INTERNET"; - case CODE_SSL_ERROR: return "CODE_SSL_ERROR"; - case CODE_ARCHIVED: return "CODE_ARCHIVED"; - case CODE_MAINTENANCE: return "CODE_MAINTENANCE"; - case CODE_LOGIN_ERROR: return "CODE_LOGIN_ERROR"; - case CODE_ACCOUNT_MISMATCH: return "CODE_ACCOUNT_MISMATCH"; - case CODE_APP_SERVER_ERROR: return "CODE_APP_SERVER_ERROR"; - case CODE_MULTIACCOUNT_SETUP: return "CODE_MULTIACCOUNT_SETUP"; - case CODE_TIMEOUT: return "CODE_TIMEOUT"; - case CODE_PROFILE_NOT_FOUND: return "CODE_PROFILE_NOT_FOUND"; - case CODE_INVALID_LOGIN: return "CODE_INVALID_LOGIN"; - case CODE_INVALID_SERVER_ADDRESS: return "CODE_INVALID_SERVER_ADDRESS"; - case CODE_INVALID_SCHOOL_NAME: return "CODE_INVALID_SCHOOL_NAME"; - case CODE_INVALID_DEVICE: return "CODE_INVALID_DEVICE"; - case CODE_OLD_PASSWORD: return "CODE_OLD_PASSWORD"; - case CODE_INVALID_TOKEN: return "CODE_INVALID_TOKEN"; - case CODE_EXPIRED_TOKEN: return "CODE_EXPIRED_TOKEN"; - case CODE_INVALID_SYMBOL: return "CODE_INVALID_SYMBOL"; - case CODE_INVALID_PIN: return "CODE_INVALID_PIN"; - case CODE_ATTACHMENT_NOT_AVAILABLE: return "CODE_ATTACHMENT_NOT_AVAILABLE"; - case CODE_LIBRUS_NOT_ACTIVATED: return "CODE_LIBRUS_NOT_ACTIVATED"; - case CODE_PROFILE_ARCHIVED: return "CODE_PROFILE_ARCHIVED"; - case CODE_LIBRUS_DISCONNECTED: return "CODE_LIBRUS_DISCONNECTED"; - case CODE_SYNERGIA_NOT_ACTIVATED: return "CODE_SYNERGIA_NOT_ACTIVATED"; - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt new file mode 100644 index 00000000..b11f6969 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-19. + */ + +package pl.szczodrzynski.edziennik.data.api + +import android.os.Build +import pl.szczodrzynski.edziennik.BuildConfig + +const val GET = 0 +const val POST = 1 + +val SYSTEM_USER_AGENT = System.getProperty("http.agent") ?: "Dalvik/2.1.0 Android" + +val SERVER_USER_AGENT = "Szkolny.eu/${BuildConfig.VERSION_NAME} $SYSTEM_USER_AGENT" + +const val FAKE_LIBRUS_API = "http://librus.szkolny.eu/api" +const val FAKE_LIBRUS_PORTAL = "http://librus.szkolny.eu" +const val FAKE_LIBRUS_AUTHORIZE = "http://librus.szkolny.eu/authorize.php" +const val FAKE_LIBRUS_LOGIN = "http://librus.szkolny.eu/login_action.php" +const val FAKE_LIBRUS_TOKEN = "http://librus.szkolny.eu/access_token.php" +const val FAKE_LIBRUS_ACCOUNT = "/synergia_accounts_fresh.php?login=" +const val FAKE_LIBRUS_ACCOUNTS = "/synergia_accounts.php" + +val LIBRUS_USER_AGENT = "$SYSTEM_USER_AGENT LibrusMobileApp" +const val SYNERGIA_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/62.0" +const val LIBRUS_CLIENT_ID = "wmSyUMo8llDAs4y9tJVYY92oyZ6h4lAt7KCuy0Gv" +const val LIBRUS_REDIRECT_URL = "http://localhost/bar" +const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/oauth2/authorize?client_id=$LIBRUS_CLIENT_ID&redirect_uri=$LIBRUS_REDIRECT_URL&response_type=code" +const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/rodzina/login/action" +const val LIBRUS_TOKEN_URL = "https://portal.librus.pl/oauth2/access_token" + +const val LIBRUS_ACCOUNT_URL = "/v2/SynergiaAccounts/fresh/" // + login +const val LIBRUS_ACCOUNTS_URL = "/v2/SynergiaAccounts" + +/** https://api.librus.pl/2.0 */ +const val LIBRUS_API_URL = "https://api.librus.pl/2.0" +/** https://portal.librus.pl/api */ +const val LIBRUS_PORTAL_URL = "https://portal.librus.pl/api" +/** https://api.librus.pl/OAuth/Token */ +const val LIBRUS_API_TOKEN_URL = "https://api.librus.pl/OAuth/Token" +/** https://api.librus.pl/OAuth/TokenJST */ +const val LIBRUS_API_TOKEN_JST_URL = "https://api.librus.pl/OAuth/TokenJST" +const val LIBRUS_API_AUTHORIZATION = "Mjg6ODRmZGQzYTg3YjAzZDNlYTZmZmU3NzdiNThiMzMyYjE=" +const val LIBRUS_API_SECRET_JST = "18b7c1ee08216f636a1b1a2440e68398" +const val LIBRUS_API_CLIENT_ID_JST = "49" +//const val LIBRUS_API_CLIENT_ID_JST_REFRESH = "42" + +const val LIBRUS_JST_DEMO_CODE = "68656A21" +const val LIBRUS_JST_DEMO_PIN = "1290" + +const val LIBRUS_SYNERGIA_URL = "https://synergia.librus.pl" +/** https://synergia.librus.pl/loguj/token/TOKEN/przenies */ +const val LIBRUS_SYNERGIA_TOKEN_LOGIN_URL = "https://synergia.librus.pl/loguj/token/TOKEN/przenies" + +const val LIBRUS_MESSAGES_URL = "https://wiadomosci.librus.pl/module" +const val LIBRUS_SANDBOX_URL = "https://sandbox.librus.pl/index.php?action=" + +const val IDZIENNIK_USER_AGENT = SYNERGIA_USER_AGENT +const val IDZIENNIK_WEB_URL = "https://iuczniowie.progman.pl/idziennik" +const val IDZIENNIK_WEB_LOGIN = "login.aspx" +const val IDZIENNIK_WEB_SETTINGS = "mod_panelRodzica/Ustawienia.aspx" +const val IDZIENNIK_WEB_TIMETABLE = "mod_panelRodzica/plan/WS_Plan.asmx/pobierzPlanZajec" +const val IDZIENNIK_WEB_GRADES = "mod_panelRodzica/oceny/WS_ocenyUcznia.asmx/pobierzOcenyUcznia" +const val IDZIENNIK_WEB_MISSING_GRADES = "mod_panelRodzica/brak_ocen/WS_BrakOcenUcznia.asmx/pobierzBrakujaceOcenyUcznia" +const val IDZIENNIK_WEB_EXAMS = "mod_panelRodzica/sprawdziany/mod_sprawdzianyPanel.asmx/pobierzListe" +const val IDZIENNIK_WEB_HOMEWORK = "mod_panelRodzica/pracaDomowa/WS_pracaDomowa.asmx/pobierzPraceDomowe" +const val IDZIENNIK_WEB_NOTICES = "mod_panelRodzica/uwagi/WS_uwagiUcznia.asmx/pobierzUwagiUcznia" +const val IDZIENNIK_WEB_ATTENDANCE = "mod_panelRodzica/obecnosci/WS_obecnosciUcznia.asmx/pobierzObecnosciUcznia" +const val IDZIENNIK_WEB_ANNOUNCEMENTS = "mod_panelRodzica/tabOgl/WS_tablicaOgloszen.asmx/GetOgloszenia" +const val IDZIENNIK_WEB_MESSAGES_LIST = "mod_komunikator/WS_wiadomosci.asmx/PobierzListeWiadomosci" +const val IDZIENNIK_WEB_GET_MESSAGE = "mod_komunikator/WS_wiadomosci.asmx/PobierzWiadomosc" +const val IDZIENNIK_WEB_GET_RECIPIENT_LIST = "mod_komunikator/WS_wiadomosci.asmx/pobierzListeOdbiorcowPanelRodzic" +const val IDZIENNIK_WEB_SEND_MESSAGE = "mod_komunikator/WS_wiadomosci.asmx/WyslijWiadomosc" +const val IDZIENNIK_WEB_GET_ATTACHMENT = "mod_komunikator/Download.ashx" + +val IDZIENNIK_API_USER_AGENT = SYSTEM_USER_AGENT +const val IDZIENNIK_API_URL = "https://iuczniowie.progman.pl/idziennik/api" +const val IDZIENNIK_API_CURRENT_REGISTER = "Uczniowie/\$STUDENT_ID/AktualnyDziennik" +const val IDZIENNIK_API_GRADES = "Uczniowie/\$STUDENT_ID/Oceny/" /* + semester */ +const val IDZIENNIK_API_MESSAGES_INBOX = "Wiadomosci/Odebrane" +const val IDZIENNIK_API_MESSAGES_SENT = "Wiadomosci/Wyslane" + + +val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT + +const val VULCAN_API_USER_AGENT = "MobileUserAgent" +const val VULCAN_API_APP_NAME = "VULCAN-Android-ModulUcznia" +const val VULCAN_API_APP_VERSION = "19.4.1.436" +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_API_ENDPOINT_CERTIFICATE = "mobile-api/Uczen.v3.UczenStart/Certyfikat" +const val VULCAN_API_ENDPOINT_STUDENT_LIST = "mobile-api/Uczen.v3.UczenStart/ListaUczniow" +const val VULCAN_API_ENDPOINT_DICTIONARIES = "mobile-api/Uczen.v3.Uczen/Slowniki" +const val VULCAN_API_ENDPOINT_TIMETABLE = "mobile-api/Uczen.v3.Uczen/PlanLekcjiZeZmianami" +const val VULCAN_API_ENDPOINT_GRADES = "mobile-api/Uczen.v3.Uczen/Oceny" +const val VULCAN_API_ENDPOINT_GRADES_PROPOSITIONS = "mobile-api/Uczen.v3.Uczen/OcenyPodsumowanie" +const val VULCAN_API_ENDPOINT_EVENTS = "mobile-api/Uczen.v3.Uczen/Sprawdziany" +const val VULCAN_API_ENDPOINT_HOMEWORK = "mobile-api/Uczen.v3.Uczen/ZadaniaDomowe" +const val VULCAN_API_ENDPOINT_NOTICES = "mobile-api/Uczen.v3.Uczen/UwagiUcznia" +const val VULCAN_API_ENDPOINT_ATTENDANCE = "mobile-api/Uczen.v3.Uczen/Frekwencje" +const val VULCAN_API_ENDPOINT_MESSAGES_RECEIVED = "mobile-api/Uczen.v3.Uczen/WiadomosciOdebrane" +const val VULCAN_API_ENDPOINT_MESSAGES_SENT = "mobile-api/Uczen.v3.Uczen/WiadomosciWyslane" +const val VULCAN_API_ENDPOINT_MESSAGES_CHANGE_STATUS = "mobile-api/Uczen.v3.Uczen/ZmienStatusWiadomosci" +const val VULCAN_API_ENDPOINT_MESSAGES_ADD = "mobile-api/Uczen.v3.Uczen/DodajWiadomosc" +const val VULCAN_API_ENDPOINT_PUSH = "mobile-api/Uczen.v3.Uczen/UstawPushToken" + +const val EDUDZIENNIK_USER_AGENT = "Szkolny.eu/${BuildConfig.VERSION_NAME}" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/DataNotifications.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/DataNotifications.kt new file mode 100644 index 00000000..d416e858 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/DataNotifications.kt @@ -0,0 +1,210 @@ +package pl.szczodrzynski.edziennik.data.api + +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_AGENDA +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ANNOUNCEMENTS +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ATTENDANCE +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_BEHAVIOUR +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_GRADES +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOME +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOMEWORK +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.models.Data +import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.* +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.notices.Notice +import pl.szczodrzynski.edziennik.data.db.modules.notification.Notification +import pl.szczodrzynski.edziennik.data.db.modules.notification.Notification.Companion.TYPE_LUCKY_NUMBER +import pl.szczodrzynski.edziennik.data.db.modules.notification.Notification.Companion.TYPE_NEW_ANNOUNCEMENT +import pl.szczodrzynski.edziennik.data.db.modules.notification.Notification.Companion.TYPE_NEW_ATTENDANCE +import pl.szczodrzynski.edziennik.data.db.modules.notification.Notification.Companion.TYPE_NEW_EVENT +import pl.szczodrzynski.edziennik.data.db.modules.notification.Notification.Companion.TYPE_NEW_GRADE +import pl.szczodrzynski.edziennik.data.db.modules.notification.Notification.Companion.TYPE_NEW_HOMEWORK +import pl.szczodrzynski.edziennik.data.db.modules.notification.Notification.Companion.TYPE_NEW_MESSAGE +import pl.szczodrzynski.edziennik.data.db.modules.notification.Notification.Companion.TYPE_NEW_NOTICE +import pl.szczodrzynski.edziennik.data.db.modules.notification.Notification.Companion.TYPE_TIMETABLE_LESSON_CHANGE +import pl.szczodrzynski.edziennik.data.db.modules.notification.getNotificationTitle +import pl.szczodrzynski.edziennik.utils.models.Date + +class DataNotifications(val data: Data) { + companion object { + private const val TAG = "DataNotifications" + } + + val app = data.app + val profileId = data.profile?.id ?: -1 + val profileName = data.profile?.name ?: "" + val profile = data.profile + val loginStore = data.loginStore + + init { run { + if (profile == null) { + return@run + } + + val today = Date.getToday() + val todayValue = today.value + profile.currentSemester = profile.dateToSemester(today) + + for (lesson in app.db.timetableDao().getNotNotifiedNow(profileId)) { + val text = app.getString(R.string.notification_lesson_change_format, lesson.getDisplayChangeType(app), if (lesson.displayDate == null) "" else lesson.displayDate!!.formattedString, lesson.changeSubjectName) + data.notifications += Notification( + title = app.getNotificationTitle(TYPE_TIMETABLE_LESSON_CHANGE), + text = text, + type = TYPE_TIMETABLE_LESSON_CHANGE, + profileId = profileId, + profileName = profileName, + viewId = DRAWER_ITEM_TIMETABLE, + addedDate = lesson.addedDate + ).addExtra("timetableDate", lesson.displayDate?.stringY_m_d ?: "") + } + + for (event in app.db.eventDao().getNotNotifiedNow(profileId)) { + val text = if (event.type == Event.TYPE_HOMEWORK) + app.getString( + if (event.subjectLongName.isNullOrEmpty()) + R.string.notification_homework_no_subject_format + else + R.string.notification_homework_format, + event.subjectLongName, + event.eventDate.formattedString + ) + else + app.getString( + if (event.subjectLongName.isNullOrEmpty()) + R.string.notification_event_no_subject_format + else + R.string.notification_event_format, + event.typeName, + event.eventDate.formattedString, + event.subjectLongName + ) + val type = if (event.type == Event.TYPE_HOMEWORK) TYPE_NEW_HOMEWORK else TYPE_NEW_EVENT + data.notifications += Notification( + title = app.getNotificationTitle(type), + text = text, + type = type, + profileId = profileId, + profileName = profileName, + viewId = if (event.type == Event.TYPE_HOMEWORK) DRAWER_ITEM_HOMEWORK else DRAWER_ITEM_AGENDA, + addedDate = event.addedDate + ).addExtra("eventId", event.id).addExtra("eventDate", event.eventDate.value.toLong()) + } + + for (grade in app.db.gradeDao().getNotNotifiedNow(profileId)) { + val gradeName = when (grade.type) { + TYPE_SEMESTER1_PROPOSED, TYPE_SEMESTER2_PROPOSED -> app.getString(R.string.grade_semester_proposed_format_2, grade.name) + TYPE_SEMESTER1_FINAL, TYPE_SEMESTER2_FINAL -> app.getString(R.string.grade_semester_final_format_2, grade.name) + TYPE_YEAR_PROPOSED -> app.getString(R.string.grade_year_proposed_format_2, grade.name) + TYPE_YEAR_FINAL -> app.getString(R.string.grade_year_final_format_2, grade.name) + else -> grade.name + } + val text = app.getString(R.string.notification_grade_format, gradeName, grade.subjectLongName) + data.notifications += Notification( + title = app.getNotificationTitle(TYPE_NEW_GRADE), + text = text, + type = TYPE_NEW_GRADE, + profileId = profileId, + profileName = profileName, + viewId = DRAWER_ITEM_GRADES, + addedDate = grade.addedDate + ).addExtra("gradeId", grade.id).addExtra("gradesSubjectId", grade.subjectId) + } + + for (notice in app.db.noticeDao().getNotNotifiedNow(profileId)) { + val noticeTypeStr = if (notice.type == Notice.TYPE_POSITIVE) app.getString(R.string.notification_notice_praise) else if (notice.type == Notice.TYPE_NEGATIVE) app.getString(R.string.notification_notice_warning) else app.getString(R.string.notification_notice_new) + val text = app.getString(R.string.notification_notice_format, noticeTypeStr, notice.teacherFullName, Date.fromMillis(notice.addedDate).formattedString) + data.notifications += Notification( + title = app.getNotificationTitle(TYPE_NEW_NOTICE), + text = text, + type = TYPE_NEW_NOTICE, + profileId = profileId, + profileName = profileName, + viewId = DRAWER_ITEM_BEHAVIOUR, + addedDate = notice.addedDate + ).addExtra("noticeId", notice.id) + } + + for (attendance in app.db.attendanceDao().getNotNotifiedNow(profileId)) { + var attendanceTypeStr = app.getString(R.string.notification_type_attendance) + when (attendance.type) { + Attendance.TYPE_ABSENT -> attendanceTypeStr = app.getString(R.string.notification_absence) + Attendance.TYPE_ABSENT_EXCUSED -> attendanceTypeStr = app.getString(R.string.notification_absence_excused) + Attendance.TYPE_BELATED -> attendanceTypeStr = app.getString(R.string.notification_belated) + Attendance.TYPE_BELATED_EXCUSED -> attendanceTypeStr = app.getString(R.string.notification_belated_excused) + Attendance.TYPE_RELEASED -> attendanceTypeStr = app.getString(R.string.notification_release) + } + val text = app.getString( + if (attendance.subjectLongName.isNullOrEmpty()) + R.string.notification_attendance_no_lesson_format + else + R.string.notification_attendance_format, + attendanceTypeStr, + attendance.subjectLongName, + attendance.lessonDate.formattedString + ) + data.notifications += Notification( + title = app.getNotificationTitle(TYPE_NEW_ATTENDANCE), + text = text, + type = TYPE_NEW_ATTENDANCE, + profileId = profileId, + profileName = profileName, + viewId = DRAWER_ITEM_ATTENDANCE, + addedDate = attendance.addedDate + ).addExtra("attendanceId", attendance.id).addExtra("attendanceSubjectId", attendance.subjectId) + } + + for (announcement in app.db.announcementDao().getNotNotifiedNow(profileId)) { + val text = app.context.getString(R.string.notification_announcement_format, announcement.subject) + data.notifications += Notification( + title = app.getNotificationTitle(TYPE_NEW_ANNOUNCEMENT), + text = text, + type = TYPE_NEW_ANNOUNCEMENT, + profileId = profileId, + profileName = profileName, + viewId = DRAWER_ITEM_ANNOUNCEMENTS, + addedDate = announcement.addedDate + ).addExtra("announcementId", announcement.id) + } + + for (message in app.db.messageDao().getReceivedNotNotifiedNow(profileId)) { + val text = app.context.getString(R.string.notification_message_format, message.senderFullName, message.subject) + data.notifications += Notification( + title = app.getNotificationTitle(TYPE_NEW_MESSAGE), + text = text, + type = TYPE_NEW_MESSAGE, + profileId = profileId, + profileName = profileName, + viewId = DRAWER_ITEM_MESSAGES, + addedDate = message.addedDate + ).addExtra("messageType", Message.TYPE_RECEIVED.toLong()).addExtra("messageId", message.id) + } + + val luckyNumbers = app.db.luckyNumberDao().getNotNotifiedNow(profileId) + luckyNumbers?.removeAll { it.date < today } + luckyNumbers?.forEach { luckyNumber -> + val text = when (luckyNumber.date.value) { + todayValue -> // LN for today + app.getString(if (profile.studentNumber != -1 && profile.studentNumber == luckyNumber.number) R.string.notification_lucky_number_yours_format else R.string.notification_lucky_number_format, luckyNumber.number) + todayValue + 1 -> // LN for tomorrow + app.getString(if (profile.studentNumber != -1 && profile.studentNumber == luckyNumber.number) R.string.notification_lucky_number_yours_tomorrow_format else R.string.notification_lucky_number_tomorrow_format, luckyNumber.number) + else -> // LN for later + app.getString(if (profile.studentNumber != -1 && profile.studentNumber == luckyNumber.number) R.string.notification_lucky_number_yours_later_format else R.string.notification_lucky_number_later_format, luckyNumber.date.formattedString, luckyNumber.number) + } + data.notifications += Notification( + title = app.getNotificationTitle(TYPE_LUCKY_NUMBER), + text = text, + type = TYPE_LUCKY_NUMBER, + profileId = profileId, + profileName = profileName, + viewId = DRAWER_ITEM_HOME, + addedDate = luckyNumber.addedDate + ) + } + + data.db.metadataDao().setAllNotified(profileId, true) + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Edziennik.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Edziennik.java deleted file mode 100644 index 99ab1a4b..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Edziennik.java +++ /dev/null @@ -1,1171 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api; - -import android.app.Activity; -import android.appwidget.AppWidgetManager; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.text.Html; -import android.util.Base64; -import android.util.Log; -import android.webkit.CookieManager; -import android.webkit.CookieSyncManager; -import android.webkit.WebView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; - -import com.afollestad.materialdialogs.DialogAction; -import com.afollestad.materialdialogs.MaterialDialog; -import com.danimahardhika.cafebar.CafeBar; -import com.google.android.gms.common.util.ArrayUtils; -import com.google.firebase.iid.FirebaseInstanceId; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.mikepenz.iconics.IconicsDrawable; -import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import pl.szczodrzynski.edziennik.App; -import pl.szczodrzynski.edziennik.BuildConfig; -import pl.szczodrzynski.edziennik.R; -import pl.szczodrzynski.edziennik.MainActivity; -import pl.szczodrzynski.edziennik.WidgetTimetable; -import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface; -import pl.szczodrzynski.edziennik.data.api.interfaces.SyncCallback; -import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull; -import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance; -import pl.szczodrzynski.edziennik.data.db.modules.attendance.AttendanceFull; -import pl.szczodrzynski.edziennik.data.db.modules.events.Event; -import pl.szczodrzynski.edziennik.data.db.modules.events.EventFull; -import pl.szczodrzynski.edziennik.data.db.modules.events.EventType; -import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeFull; -import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonFull; -import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore; -import pl.szczodrzynski.edziennik.data.db.modules.messages.Message; -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull; -import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata; -import pl.szczodrzynski.edziennik.data.db.modules.notices.Notice; -import pl.szczodrzynski.edziennik.data.db.modules.notices.NoticeFull; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull; -import pl.szczodrzynski.edziennik.data.db.modules.teams.Team; -import pl.szczodrzynski.edziennik.utils.models.Date; -import pl.szczodrzynski.edziennik.utils.models.Notification; -import pl.szczodrzynski.edziennik.network.ServerRequest; -import pl.szczodrzynski.edziennik.sync.SyncJob; -import pl.szczodrzynski.edziennik.utils.Themes; -import pl.szczodrzynski.edziennik.widgets.luckynumber.WidgetLuckyNumber; -import pl.szczodrzynski.edziennik.widgets.notifications.WidgetNotifications; - -import static android.content.Context.CLIPBOARD_SERVICE; -import static com.mikepenz.iconics.utils.IconicsConvertersKt.colorInt; -import static com.mikepenz.iconics.utils.IconicsConvertersKt.sizeDp; -import static pl.szczodrzynski.edziennik.App.APP_URL; -import static pl.szczodrzynski.edziennik.MainActivity.DRAWER_ITEM_HOME; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_OK; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_OTHER; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_PROFILE_ARCHIVED; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_PROFILE_NOT_FOUND; -import static pl.szczodrzynski.edziennik.data.api.AppError.stringErrorCode; -import static pl.szczodrzynski.edziennik.data.api.AppError.stringErrorType; -import static pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface.FEATURE_AGENDA; -import static pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface.FEATURE_ALL; -import static pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface.FEATURE_ANNOUNCEMENTS; -import static pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface.FEATURE_ATTENDANCE; -import static pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface.FEATURE_GRADES; -import static pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface.FEATURE_HOMEWORK; -import static pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface.FEATURE_MESSAGES_INBOX; -import static pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface.FEATURE_MESSAGES_OUTBOX; -import static pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface.FEATURE_NOTICES; -import static pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface.FEATURE_TIMETABLE; -import static pl.szczodrzynski.edziennik.data.db.modules.events.Event.TYPE_HOMEWORK; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER1_FINAL; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER1_PROPOSED; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER2_FINAL; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER2_PROPOSED; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_YEAR_FINAL; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_YEAR_PROPOSED; -import static pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_IUCZNIOWIE; -import static pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_LIBRUS; -import static pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_MOBIDZIENNIK; -import static pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_VULCAN; -import static pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile.REGISTRATION_ENABLED; -import static pl.szczodrzynski.edziennik.sync.SyncService.PROFILE_MAX_PROGRESS; -import static pl.szczodrzynski.edziennik.utils.Utils.d; -import static pl.szczodrzynski.edziennik.utils.Utils.ns; - -public class Edziennik { - //public static final int CODE_NULL = 0; - private App app; - - private static final String TAG = "Edziennik"; - private static boolean registerEmpty; - public static int oldLuckyNumber; - - public static EdziennikInterface getApi(App app, int loginType) { - switch (loginType) { - default: - case LOGIN_TYPE_MOBIDZIENNIK: - return app.apiMobidziennik; - case LOGIN_TYPE_LIBRUS: - return app.apiLibrus; - case LOGIN_TYPE_IUCZNIOWIE: - return app.apiIuczniowie; - case LOGIN_TYPE_VULCAN: - return app.apiVulcan; - } - } - - public Edziennik(App app) { - this.app = app; - } - - @SuppressWarnings("deprecation") - public static void clearCookies(Context context, String url) { - //Log.d(TAG, "Cookies: " + yahooCookies); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - //Log.d(TAG, "Using clearCookies code for API >=" + String.valueOf(Build.VERSION_CODES.LOLLIPOP_MR1)); - CookieManager.getInstance().removeAllCookies(null); - CookieManager.getInstance().flush(); - } else { - //Log.d(TAG, "Using clearCookies code for API <" + String.valueOf(Build.VERSION_CODES.LOLLIPOP_MR1)); - CookieSyncManager cookieSyncManager = CookieSyncManager.createInstance(context); - cookieSyncManager.startSync(); - CookieManager cookieManager = CookieManager.getInstance(); - cookieManager.removeAllCookie(); - cookieManager.removeSessionCookie(); - cookieSyncManager.stopSync(); - cookieSyncManager.sync(); - } - } - - public void initMessagesWebView(WebView webView, App app, boolean fullVersion, boolean clearCookies) { - if (!app.profile.getEmpty() && app.profile.getLoginStoreType() == LoginStore.LOGIN_TYPE_MOBIDZIENNIK) { - String url; - if (fullVersion) { - url = "https://" + app.profile.getLoginData("serverName", "") + ".mobidziennik.pl/mobile/wiadomosci"; - } else { - url = "https://" + app.profile.getLoginData("serverName", "") + ".mobidziennik.pl/api/"; - } - - String str1 = "login=" + app.profile.getLoginData("username", "") + "&haslo=" + app.profile.getLoginData("password", ""); - - if (!fullVersion) { - str1 += "&ip=" + app.deviceId + "&wersja=20&token=&webview_wiadomosci=1"; - } - - - if (-1L != -1L) { - str1 += "&id_wiadomosci=" + -1L; - } - - if (clearCookies) - clearCookies(app, "https://" + app.profile.getLoginData("serverName", "") + ".mobidziennik.pl"); - - //Toast.makeText(app, "URL "+url, Toast.LENGTH_SHORT).show(); - webView.postUrl(url, str1.getBytes()); - } else if (!app.profile.getEmpty() && app.profile.getLoginStoreType() == LoginStore.LOGIN_TYPE_IUCZNIOWIE) { - String url = "https://iuczniowie.progman.pl/idziennik/mod_panelRodzica/Komunikator.aspx"; - webView.loadUrl(url); - /* - if (app.profile.loginServerName.equals("") || app.profile.loginUsername.equals("") || app.profile.loginPassword.equals("")) { - webView.loadData("

"+app.getString(R.string.api_error_code_invalid_login)+"

", "text/html", "UTF-8"); - return; - } - - if (app.appConfig.deviceId == null || app.appConfig.deviceId.equals("")) { - app.appConfig.deviceId = Settings.Secure.getString(app.getContentResolver(), Settings.Secure.ANDROID_ID); - app.appConfig.savePending = true; - } - - //Ion.getDefault(app.getContext()).getCookieMiddleware().getCookieStore().removeAll(); // TODO remove only cookies for this domain - String finalLoginServerName = app.profile.loginServerName; - String finalLoginUsername = app.profile.loginUsername; - String finalLoginPassword = app.profile.loginPassword; - Ion.with(app.getContext()) - .load("https://iuczniowie.progman.pl/idziennik/login.aspx") - .setTimeout(REQUEST_TIMEOUT) - .setHeader("User-Agent", Iuczniowie.userAgent) - //.setHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") - .asString() - .setCallback((e, result) -> { - if (e instanceof java.util.concurrent.TimeoutException) { - webView.loadData("

"+app.getString(R.string.api_error_code_timeout)+"

", "text/html", "UTF-8"); - return; - } - app.profile.loggedIn = (result != null && result.equals("ok")); - if (result == null || result.equals("")) { // for safety - webView.loadData("

"+app.getString(R.string.api_error_code_no_internet)+"

", "text/html", "UTF-8"); - return; - } - if (clearCookies) - clearCookies(app, "https://iuczniowie.progman.pl"); - - String post = ""; - try { - post += "ctl00$ContentPlaceHolder$nazwaPrzegladarki="+URLEncoder.encode(Iuczniowie.userAgent, "UTF-8"); - post += "ctl00$ContentPlaceHolder$NazwaSzkoly="+finalLoginServerName; - post += "ctl00$ContentPlaceHolder$UserName="+URLEncoder.encode(finalLoginUsername, "UTF-8"); - post += "ctl00$ContentPlaceHolder$Password="+URLEncoder.encode(finalLoginPassword, "UTF-8"); - post += "ctl00$ContentPlaceHolder$Logowanie=Zaloguj"; - } catch (UnsupportedEncodingException e1) { - e1.printStackTrace(); - } - - webView.getSettings().setUserAgentString(Iuczniowie.userAgent); - - });*/ - } else if (app.profile.getEmpty()) { - webView.loadData("

" + app.getString(R.string.sync_error_invalid_login) + "

", "text/html", "UTF-8"); - } else { - webView.loadData("

" + app.getString(R.string.settings_register_login_not_implemented_text) + "

", "text/html", "UTF-8"); - } - } - - - - /* _____ _______ _ - | __ \ /\ |__ __| | | - | |__) | __ ___ ___ ___ ___ ___ / \ ___ _ _ _ __ ___| | __ _ ___| | __ - | ___/ '__/ _ \ / __/ _ \/ __/ __| / /\ \ / __| | | | '_ \ / __| |/ _` / __| |/ / - | | | | | (_) | (_| __/\__ \__ \/ ____ \\__ \ |_| | | | | (__| | (_| \__ \ < - |_| |_| \___/ \___\___||___/___/_/ \_\___/\__, |_| |_|\___|_|\__,_|___/_|\_\ - __/ | - |__*/ - - /** - * A task for creating notifications and downloading shared events. - */ - private static class ProcessAsyncTask extends AsyncTask { - private App app; - private WeakReference activityContext; - private SyncCallback callback; - private Exception e = null; - private String apiResponse = null; - private int profileId; - private ProfileFull profile; - - public ProcessAsyncTask(App app, Context activityContext, SyncCallback callback, int profileId, ProfileFull profile) { - //d(TAG, "Thread/ProcessAsyncTask/constructor/"+Thread.currentThread().getName()); - this.app = app; - this.activityContext = new WeakReference<>(activityContext); - this.callback = callback; - this.profileId = profileId; - this.profile = profile; - } - - @Override - protected Integer doInBackground(Void... voids) { - Context activityContext = this.activityContext.get(); - //d(TAG, "Thread/ProcessAsyncTask/doInBackground/"+Thread.currentThread().getName()); - try { - - // UPDATE FCM TOKEN IF EMPTY - if (app.appConfig.fcmToken == null || app.appConfig.fcmToken.equals("")) { - FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(instanceIdResult -> { - app.appConfig.fcmToken = instanceIdResult.getToken(); - app.appConfig.savePending = true; - }); - } - - callback.onProgress(1); - if (profile.getSyncNotifications()) { - new Handler(activityContext.getMainLooper()).post(() -> { - callback.onActionStarted(R.string.sync_action_creating_notifications); - }); - - for (LessonFull change : app.db.lessonChangeDao().getNotNotifiedNow(profileId)) { - String text = app.getContext().getString(R.string.notification_lesson_change_format, change.changeTypeStr(app.getContext()), change.lessonDate == null ? "" : change.lessonDate.getFormattedString(), change.subjectLongName); - app.notifier.add(new Notification(app.getContext(), text) - .withProfileData(profile.getId(), profile.getName()) - .withType(Notification.TYPE_TIMETABLE_LESSON_CHANGE) - .withFragmentRedirect(MainActivity.DRAWER_ITEM_TIMETABLE) - .withLongExtra("timetableDate", change.lessonDate.getValue()) - .withAddedDate(change.addedDate) - ); - } - for (EventFull event : app.db.eventDao().getNotNotifiedNow(profileId)) { - String text; - if (event.type == TYPE_HOMEWORK) - text = app.getContext().getString(R.string.notification_homework_format, ns(app.getString(R.string.notification_event_no_subject), event.subjectLongName), event.eventDate.getFormattedString()); - else - text = app.getContext().getString(R.string.notification_event_format, event.typeName, event.eventDate.getFormattedString(), ns(app.getString(R.string.notification_event_no_subject), event.subjectLongName)); - app.notifier.add(new Notification(app.getContext(), text) - .withProfileData(profile.getId(), profile.getName()) - .withType(event.type == TYPE_HOMEWORK ? Notification.TYPE_NEW_HOMEWORK : Notification.TYPE_NEW_EVENT) - .withFragmentRedirect(event.type == TYPE_HOMEWORK ? MainActivity.DRAWER_ITEM_HOMEWORK : MainActivity.DRAWER_ITEM_AGENDA) - .withLongExtra("eventId", event.id) - .withLongExtra("eventDate", event.eventDate.getValue()) - .withAddedDate(event.addedDate) - ); - // student's rights abuse - disabled, because this was useless - /*if (!event.addedManually && event.type == RegisterEvent.TYPE_EXAM && event.eventDate.combineWith(event.startTime) - event.addedDate < 7 * 24 * 60 * 60 * 1000) { - text = app.getContext().getString(R.string.notification_abuse_format, event.typeString(app, app.profile), event.subjectLongName, event.eventDate.getFormattedString()); - app.notifier.add(new Notification(app.getContext(), text) - .withProfileData(profile.id, profile.name) - .withType(Notification.TYPE_GENERAL) - .withFragmentRedirect(MainActivity.DRAWER_ITEM_NOTIFICATIONS) - ); - }*/ - } - - Date today = Date.getToday(); - int todayValue = today.getValue(); - profile.setCurrentSemester(profile.dateToSemester(today)); - - for (GradeFull grade : app.db.gradeDao().getNotNotifiedNow(profileId)) { - String gradeName = grade.name; - if (grade.type == TYPE_SEMESTER1_PROPOSED - || grade.type == TYPE_SEMESTER2_PROPOSED) { - gradeName = (app.getString(R.string.grade_semester_proposed_format_2, grade.name)); - } else if (grade.type == TYPE_SEMESTER1_FINAL - || grade.type == TYPE_SEMESTER2_FINAL) { - gradeName = (app.getString(R.string.grade_semester_final_format_2, grade.name)); - } else if (grade.type == TYPE_YEAR_PROPOSED) { - gradeName = (app.getString(R.string.grade_year_proposed_format_2, grade.name)); - } else if (grade.type == TYPE_YEAR_FINAL) { - gradeName = (app.getString(R.string.grade_year_final_format_2, grade.name)); - } - String text = app.getContext().getString(R.string.notification_grade_format, gradeName, grade.subjectLongName); - app.notifier.add(new Notification(app.getContext(), text) - .withProfileData(profile.getId(), profile.getName()) - .withType(Notification.TYPE_NEW_GRADE) - .withFragmentRedirect(MainActivity.DRAWER_ITEM_GRADES) - .withLongExtra("gradesSubjectId", grade.subjectId) - .withAddedDate(grade.addedDate) - ); - } - for (NoticeFull notice : app.db.noticeDao().getNotNotifiedNow(profileId)) { - String noticeTypeStr = (notice.type == Notice.TYPE_POSITIVE ? app.getString(R.string.notification_notice_praise) : (notice.type == Notice.TYPE_NEGATIVE ? app.getString(R.string.notification_notice_warning) : app.getString(R.string.notification_notice_new))); - String text = app.getContext().getString(R.string.notification_notice_format, noticeTypeStr, notice.teacherFullName, Date.fromMillis(notice.addedDate).getFormattedString()); - app.notifier.add(new Notification(app.getContext(), text) - .withProfileData(profile.getId(), profile.getName()) - .withType(Notification.TYPE_NEW_NOTICE) - .withFragmentRedirect(MainActivity.DRAWER_ITEM_BEHAVIOUR) - .withLongExtra("noticeId", notice.id) - .withAddedDate(notice.addedDate) - ); - } - for (AttendanceFull attendance : app.db.attendanceDao().getNotNotifiedNow(profileId)) { - String attendanceTypeStr = app.getString(R.string.notification_type_attendance); - switch (attendance.type) { - case Attendance.TYPE_ABSENT: - attendanceTypeStr = app.getString(R.string.notification_absence); - break; - case Attendance.TYPE_ABSENT_EXCUSED: - attendanceTypeStr = app.getString(R.string.notification_absence_excused); - break; - case Attendance.TYPE_BELATED: - attendanceTypeStr = app.getString(R.string.notification_belated); - break; - case Attendance.TYPE_BELATED_EXCUSED: - attendanceTypeStr = app.getString(R.string.notification_belated_excused); - break; - case Attendance.TYPE_RELEASED: - attendanceTypeStr = app.getString(R.string.notification_release); - break; - } - String text = app.getContext().getString(R.string.notification_attendance_format, attendanceTypeStr, attendance.subjectLongName, attendance.lessonDate.getFormattedString()); - app.notifier.add(new Notification(app.getContext(), text) - .withProfileData(profile.getId(), profile.getName()) - .withType(Notification.TYPE_NEW_ATTENDANCE) - .withFragmentRedirect(MainActivity.DRAWER_ITEM_ATTENDANCE) - .withLongExtra("attendanceId", attendance.id) - .withAddedDate(attendance.addedDate) - ); - } - for (AnnouncementFull announcement : app.db.announcementDao().getNotNotifiedNow(profileId)) { - String text = app.getContext().getString(R.string.notification_announcement_format, announcement.subject); - app.notifier.add(new Notification(app.getContext(), text) - .withProfileData(profile.getId(), profile.getName()) - .withType(Notification.TYPE_NEW_ANNOUNCEMENT) - .withFragmentRedirect(MainActivity.DRAWER_ITEM_ANNOUNCEMENTS) - .withLongExtra("announcementId", announcement.id) - .withAddedDate(announcement.addedDate) - ); - } - for (MessageFull message : app.db.messageDao().getReceivedNotNotifiedNow(profileId)) { - String text = app.getContext().getString(R.string.notification_message_format, message.senderFullName, message.subject); - app.notifier.add(new Notification(app.getContext(), text) - .withProfileData(profile.getId(), profile.getName()) - .withType(Notification.TYPE_NEW_MESSAGE) - .withFragmentRedirect(MainActivity.DRAWER_ITEM_MESSAGES) - .withLongExtra("messageType", Message.TYPE_RECEIVED) - .withLongExtra("messageId", message.id) - .withAddedDate(message.addedDate) - ); - } - - if (profile.getLuckyNumber() != oldLuckyNumber - && profile.getLuckyNumber() != -1 - && profile.getLuckyNumberDate() != null - && profile.getLuckyNumberDate().getValue() >= todayValue) { - String text; - if (profile.getLuckyNumberDate().getValue() == todayValue) { // LN for today - text = app.getString((profile.getStudentNumber() != -1 && profile.getStudentNumber() == profile.getLuckyNumber() ? R.string.notification_lucky_number_yours_format : R.string.notification_lucky_number_format), profile.getLuckyNumber()); - } else if (profile.getLuckyNumberDate().getValue() == todayValue + 1) { // LN for tomorrow - text = app.getString((profile.getStudentNumber() != -1 && profile.getStudentNumber() == profile.getLuckyNumber() ? R.string.notification_lucky_number_yours_tomorrow_format : R.string.notification_lucky_number_tomorrow_format), profile.getLuckyNumber()); - } else { // LN for later - text = app.getString((profile.getStudentNumber() != -1 && profile.getStudentNumber() == profile.getLuckyNumber() ? R.string.notification_lucky_number_yours_later_format : R.string.notification_lucky_number_later_format), profile.getLuckyNumberDate().getFormattedString(), profile.getLuckyNumber()); - } - app.notifier.add(new Notification(app.getContext(), text) - .withProfileData(profile.getId(), profile.getName()) - .withType(Notification.TYPE_LUCKY_NUMBER) - .withFragmentRedirect(MainActivity.DRAWER_ITEM_HOME) - ); - oldLuckyNumber = profile.getLuckyNumber(); - } - } - - - app.db.metadataDao().setAllNotified(profileId, true); - callback.onProgress(1); - - // SEND WEB PUSH, if registration allowed - // otherwise, UNREGISTER THE USER - if (profile.getRegistration() == REGISTRATION_ENABLED) { - new Handler(activityContext.getMainLooper()).post(() -> { - callback.onActionStarted(R.string.sync_action_syncing_shared_events); - }); - //if (profile.registrationUsername == null || profile.registrationUsername.equals("")) { - //} - ServerRequest syncRequest = new ServerRequest(app, app.requestScheme + APP_URL + "main.php?sync", "Edziennik/REG", profile); - - if (registerEmpty) { - syncRequest.setBodyParameter("first_run", "true"); - } - - // ALSO SEND NEW DATA TO BROWSER *excluding* all Shared Events !!! - // because they will be sent by the server, as soon as it's shared, by FCM - - if (app.appConfig.webPushEnabled) { - int position = 0; - for (Notification notification : app.appConfig.notifications) { - //Log.d(TAG, notification.text); - if (!notification.notified) { - if (notification.type != Notification.TYPE_NEW_SHARED_EVENT - && notification.type != Notification.TYPE_SERVER_MESSAGE - && notification.type != Notification.TYPE_NEW_SHARED_HOMEWORK) // these are automatically sent to the browser by the server - { - //Log.d(TAG, "Adding notify[" + position + "]"); - syncRequest.setBodyParameter("notify[" + position + "][type]", Integer.toString(notification.type)); - syncRequest.setBodyParameter("notify[" + position + "][title]", notification.title); - syncRequest.setBodyParameter("notify[" + position + "][text]", notification.text); - position++; - } - } - } - } - - callback.onProgress(1); - - if (app.appConfig.webPushEnabled || profile.getEnableSharedEvents()) { - JsonObject result = syncRequest.runSync(); - callback.onProgress(1); - //Log.d(TAG, "Executed request"); - if (result == null) { - return AppError.CODE_APP_SERVER_ERROR; - } - apiResponse = result.toString(); - if (!result.get("success").getAsString().equals("true")) { - return AppError.CODE_APP_SERVER_ERROR; - } - // HERE PROCESS ALL THE RECEIVED EVENTS - // add them to the profile and create appropriate notifications - for (JsonElement jEventEl : result.getAsJsonArray("events")) { - JsonObject jEvent = jEventEl.getAsJsonObject(); - String teamCode = jEvent.get("team").getAsString(); - //d(TAG, "An event is there! "+jEvent.toString()); - // get the target Team from teamCode - Team team = app.db.teamDao().getByCodeNow(profile.getId(), teamCode); - if (team != null) { - //d(TAG, "The target team is "+team.name+", ID "+team.id); - // create the event from Json. Add the missing teamId and !!profileId!! - Event event = app.gson.fromJson(jEvent.toString(), Event.class); - // proguard. disable for Event.class - if (event.eventDate == null) { - apiResponse += "\n\nEventDate == null\n" + jEvent.toString(); - throw new Exception("null eventDate"); - } - event.profileId = profile.getId(); - event.teamId = team.id; - event.addedManually = true; - //d(TAG, "Created the event! "+event); - - if (event.sharedBy != null && event.sharedBy.equals(profile.getUsernameId())) { - //d(TAG, "Shared by self! Changing name"); - event.sharedBy = "self"; - event.sharedByName = profile.getStudentNameLong(); - } - - EventType type = app.db.eventTypeDao().getByIdNow(profileId, event.type); - - //d(TAG, "Finishing adding event "+event); - app.db.eventDao().add(event); - Metadata metadata = new Metadata(profile.getId(), event.type == TYPE_HOMEWORK ? Metadata.TYPE_HOMEWORK : Metadata.TYPE_EVENT, event.id, registerEmpty, true, jEvent.get("addedDate").getAsLong()); - long metadataId = app.db.metadataDao().add(metadata); - if (metadataId != -1 && !registerEmpty) { - app.notifier.add(new Notification(app.getContext(), app.getString(R.string.notification_shared_event_format, event.sharedByName, type != null ? type.name : "wydarzenie", event.eventDate == null ? "nieznana data" : event.eventDate.getFormattedString(), event.topic)) - .withProfileData(profile.getId(), profile.getName()) - .withType(event.type == TYPE_HOMEWORK ? Notification.TYPE_NEW_SHARED_HOMEWORK : Notification.TYPE_NEW_SHARED_EVENT) - .withFragmentRedirect(event.type == TYPE_HOMEWORK ? MainActivity.DRAWER_ITEM_HOMEWORK : MainActivity.DRAWER_ITEM_AGENDA) - .withLongExtra("eventDate", event.eventDate.getValue()) - ); - } - } - } - callback.onProgress(5); - return CODE_OK; - } else { - callback.onProgress(6); - return CODE_OK; - } - } else { - // the user does not want to be registered - callback.onProgress(7); - return CODE_OK; - } - } catch (Exception e) { - e.printStackTrace(); - this.e = e; - return null; - } - //return null; - } - - @Override - protected void onPostExecute(Integer errorCode) { - //d(TAG, "Thread/ProcessAsyncTask/onPostExecute/"+Thread.currentThread().getName()); - Context activityContext = this.activityContext.get(); - app.profileSaveFull(profile); - if (app.profile != null && profile.getId() == app.profile.getId()) { - app.profile = profile; - } - if (errorCode == null) { - // this means an Exception was thrown - callback.onError(activityContext, new AppError(TAG, 513, CODE_OTHER, e, apiResponse)); - return; - } - //Log.d(TAG, "Finishing"); - - - callback.onProgress(1); - - if (errorCode == CODE_OK) - callback.onSuccess(activityContext, profile); - else { - try { - // oh that's useless - throw new RuntimeException(stringErrorCode(app, errorCode, "")); - } catch (Exception e) { - callback.onError(activityContext, new AppError(TAG, 528, errorCode, e, (String) null)); - } - } - super.onPostExecute(errorCode); - } - } - - public void notifyAndReload() { - app.notifier.postAll(null); - app.saveConfig(); - SyncJob.schedule(app); - Intent i = new Intent(Intent.ACTION_MAIN) - .putExtra("reloadProfileId", -1); - app.sendBroadcast(i); - - Intent intent = new Intent(app.getContext(), WidgetTimetable.class); - intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - int[] ids = AppWidgetManager.getInstance(app).getAppWidgetIds(new ComponentName(app, WidgetTimetable.class)); - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); - app.sendBroadcast(intent); - - intent = new Intent(app.getContext(), WidgetNotifications.class); - intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - ids = AppWidgetManager.getInstance(app).getAppWidgetIds(new ComponentName(app, WidgetNotifications.class)); - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); - app.sendBroadcast(intent); - - intent = new Intent(app.getContext(), WidgetLuckyNumber.class); - intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - ids = AppWidgetManager.getInstance(app).getAppWidgetIds(new ComponentName(app, WidgetLuckyNumber.class)); - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); - app.sendBroadcast(intent); - } - - /* _____ - / ____| - | (___ _ _ _ __ ___ - \___ \| | | | '_ \ / __| - ____) | |_| | | | | (__ - |_____/ \__, |_| |_|\___| - __/ | - |__*/ - // DataCallbacks that are *not* in Edziennik.sync need to be executed on the main thread. - // EdziennikInterface.sync is executed on a worker thread - // in Edziennik.sync/newCallback methods are called on a worker thread - - // callback passed to Edziennik.sync is executed on the main thread - // thus, callback which is in guiSync is also on the main thread - - /** - * Sync all Edziennik data. - * Used in services, login form and {@code guiSync} - *

- * May be ran on worker thread. - * {@link EdziennikInterface}.sync is ran always on worker thread. - * Every callback is ran on the UI thread. - * - * @param app - * @param activityContext - * @param callback - * @param profileId - */ - public void sync(@NonNull App app, @NonNull Context activityContext, @NonNull SyncCallback callback, int profileId) { - sync(app, activityContext, callback, profileId, (int[])null); - } - public void sync(@NonNull App app, @NonNull Context activityContext, @NonNull SyncCallback callback, int profileId, @Nullable int ... featureList) { - // empty: no unread notifications, all shared events (current+past) - // only if there is no data, and we are not logged in yet - SyncCallback newCallback = new SyncCallback() { - @Override - public void onLoginFirst(List profileList, LoginStore loginStore) { - new Handler(activityContext.getMainLooper()).post(() -> { - callback.onLoginFirst(profileList, loginStore); - }); - } - - @Override - public void onSuccess(Context activityContext, ProfileFull profileFull) { - new Handler(activityContext.getMainLooper()).post(() -> { - new ProcessAsyncTask(app, activityContext, callback, profileId, profileFull).execute(); - }); - } - - @Override - public void onError(Context activityContext, AppError error) { - new Handler(activityContext.getMainLooper()).post(() -> { - callback.onError(activityContext, error); - }); - } - - @Override - public void onProgress(int progressStep) { - new Handler(activityContext.getMainLooper()).post(() -> { - callback.onProgress(progressStep); - }); - } - - @Override - public void onActionStarted(int stringResId) { - new Handler(activityContext.getMainLooper()).post(() -> { - callback.onActionStarted(stringResId); - }); - } - }; - AsyncTask.execute(() -> { - ProfileFull profile = app.db.profileDao().getByIdNow(profileId); - if (profile != null) { - - if (profile.getArchived()) { - newCallback.onError(activityContext, new AppError(TAG, 678, CODE_PROFILE_ARCHIVED, profile.getName())); - return; - } - else if (profile.getDateYearEnd() != null && Date.getToday().getValue() >= profile.getDateYearEnd().getValue()) { - profile.setArchived(true); - app.notifier.add(new Notification(app.getContext(), app.getString(R.string.profile_auto_archiving_format, profile.getName(), profile.getDateYearEnd().getFormattedString())) - .withProfileData(profile.getId(), profile.getName()) - .withType(Notification.TYPE_AUTO_ARCHIVING) - .withFragmentRedirect(DRAWER_ITEM_HOME) - .withLongExtra("autoArchiving", 1L) - ); - app.notifier.postAll(null); - app.db.profileDao().add(profile); - if (App.profileId == profile.getId()) { - app.profile.setArchived(true); - } - newCallback.onSuccess(activityContext, profile); - return; - } - - registerEmpty = profile.getEmpty(); - oldLuckyNumber = profile.getLuckyNumber(); - getApi(app, profile.getLoginStoreType()).syncFeature(activityContext, newCallback, profile, featureList); - } else { - new Handler(activityContext.getMainLooper()).post(() -> callback.onError(activityContext, new AppError(TAG, 609, CODE_PROFILE_NOT_FOUND, (String) null))); - } - }); - } - - /* _____ _ _ _____ - / ____| | | |_ _| - | | __| | | | | | __ ___ __ __ _ _ __ _ __ ___ _ __ ___ - | | |_ | | | | | | \ \ /\ / / '__/ _` | '_ \| '_ \ / _ \ '__/ __| - | |__| | |__| |_| |_ \ V V /| | | (_| | |_) | |_) | __/ | \__ \ - \_____|\____/|_____| \_/\_/ |_| \__,_| .__/| .__/ \___|_| |___/ - | | | | - |_| |*/ - /** - * Sync all Edziennik data while showing a progress dialog. - * A wrapper for {@code sync} - * - * Does not switch between threads. - * All callbacks have to be executed on the UI thread. - * - * @param app an App singleton instance - * @param activity a parent activity - * @param profileId ID of the profile to sync - * @param dialogTitle a title of the dialog to show - * @param dialogText dialog's content - * @param successText a toast to show on success - */ - public void guiSync(@NonNull App app, @NonNull Activity activity, int profileId, @StringRes int dialogTitle, @StringRes int dialogText, @StringRes int successText) { - guiSync(app, activity, profileId, dialogTitle, dialogText, successText, (int[])null); - } - public void guiSync(@NonNull App app, @NonNull Activity activity, int profileId, @StringRes int dialogTitle, @StringRes int dialogText, @StringRes int successText, int ... featureList) { - MaterialDialog progressDialog = new MaterialDialog.Builder(activity) - .title(dialogTitle) - .content(dialogText) - .progress(false, PROFILE_MAX_PROGRESS, false) - .canceledOnTouchOutside(false) - .show(); - SyncCallback guiSyncCallback = new SyncCallback() { - @Override - public void onLoginFirst(List profileList, LoginStore loginStore) { - - } - - @Override - public void onSuccess(Context activityContext, ProfileFull profileFull) { - progressDialog.dismiss(); - Toast.makeText(activityContext, successText, Toast.LENGTH_SHORT).show(); - notifyAndReload(); - // profiles are saved automatically, during app.saveConfig in processFinish - /*if (activityContext instanceof MainActivity) { - //((MainActivity) activityContext).reloadCurrentFragment("GuiSync"); - ((MainActivity) activityContext).accountHeaderAddProfiles(); - }*/ - } - - @Override - public void onError(Context activityContext, AppError error) { - progressDialog.dismiss(); - guiShowErrorDialog((Activity) activityContext, error, R.string.sync_error_dialog_title); - } - - @Override - public void onProgress(int progressStep) { - progressDialog.incrementProgress(progressStep); - } - - @Override - public void onActionStarted(int stringResId) { - progressDialog.setContent(activity.getString(R.string.sync_action_format, activity.getString(stringResId))); - } - }; - app.apiEdziennik.sync(app, activity, guiSyncCallback, profileId, featureList); - } - /** - * Sync all Edziennik data in background. - * A callback is executed on main thread. - * A wrapper for {@code sync} - * - * @param app an App singleton instance - * @param activity a parent activity - * @param profileId ID of the profile to sync - * @param syncCallback a callback - * @param feature a feature to sync - */ - public void guiSyncSilent(@NonNull App app, @NonNull Activity activity, int profileId, SyncCallback syncCallback, int feature) { - SyncCallback guiSyncCallback = new SyncCallback() { - @Override - public void onLoginFirst(List profileList, LoginStore loginStore) { - - } - - @Override - public void onSuccess(Context activityContext, ProfileFull profileFull) { - notifyAndReload(); - syncCallback.onSuccess(activityContext, profileFull); - } - - @Override - public void onError(Context activityContext, AppError error) { - syncCallback.onError(activityContext, error); - } - - @Override - public void onProgress(int progressStep) { - syncCallback.onProgress(progressStep); - } - - @Override - public void onActionStarted(int stringResId) { - syncCallback.onActionStarted(stringResId); - } - }; - app.apiEdziennik.sync(app, activity, guiSyncCallback, profileId, feature == FEATURE_ALL ? null : new int[]{feature}); - } - - /** - * Show a dialog allowing the user to choose which features to sync. - * Handles everything including pre-selecting the features basing on the current fragment. - * - * Will execute {@code sync} after the selection is made. - * - * A normal progress dialog is shown during the sync. - * - * @param app an App singleton instance - * @param activity a parent activity - * @param profileId ID of the profile to sync - * @param dialogTitle a title of the dialog to show - * @param dialogText dialog's content - * @param successText a toast to show on success - * @param currentFeature a feature id representing the currently opened fragment or caller - */ - public void guiSyncFeature(@NonNull App app, - @NonNull Activity activity, - int profileId, - @StringRes int dialogTitle, - @StringRes int dialogText, - @StringRes int successText, - int currentFeature) { - - String[] items = new String[]{ - app.getString(R.string.menu_timetable), - app.getString(R.string.menu_agenda), - app.getString(R.string.menu_grades), - app.getString(R.string.menu_homework), - app.getString(R.string.menu_notices), - app.getString(R.string.menu_attendance), - app.getString(R.string.title_messages_inbox_single), - app.getString(R.string.title_messages_sent_single), - app.getString(R.string.menu_announcements) - }; - int[] itemsIds = new int[]{ - FEATURE_TIMETABLE, - FEATURE_AGENDA, - FEATURE_GRADES, - FEATURE_HOMEWORK, - FEATURE_NOTICES, - FEATURE_ATTENDANCE, - FEATURE_MESSAGES_INBOX, - FEATURE_MESSAGES_OUTBOX, - FEATURE_ANNOUNCEMENTS - }; - int[] selectedIndices; - if (currentFeature == FEATURE_ALL) { - selectedIndices = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8}; - } - else { - selectedIndices = new int[]{Arrays.binarySearch(itemsIds, currentFeature)}; - } - - MaterialDialog dialog = new MaterialDialog.Builder(activity) - .title(R.string.sync_feature_title) - .content(R.string.sync_feature_text) - .positiveText(R.string.ok) - .negativeText(R.string.cancel) - .neutralText(R.string.sync_feature_all) - .items(items) - .itemsIds(itemsIds) - .itemsCallbackMultiChoice(ArrayUtils.toWrapperArray(selectedIndices), (dialog1, which, text) -> { - dialog1.getActionButton(DialogAction.POSITIVE).setEnabled(which.length > 0); - return true; - }) - .alwaysCallMultiChoiceCallback() - .onPositive(((dialog1, which) -> { - List featureList = new ArrayList<>(); - for (int i: dialog1.getSelectedIndices()) { - featureList.add(itemsIds[i]); - } - guiSync(app, activity, profileId, dialogTitle, dialogText, successText, ArrayUtils.toPrimitiveArray(featureList)); - })) - .onNeutral(((dialog1, which) -> { - guiSync(app, activity, profileId, dialogTitle, dialogText, successText); - })) - .show(); - - - - } - - public void guiShowArchivedDialog(Activity activity, String profileName) { - new MaterialDialog.Builder(activity) - .title(R.string.profile_archived_dialog_title) - .content(activity.getString(R.string.profile_archived_dialog_text_format, profileName)) - .positiveText(R.string.ok) - .onPositive(((dialog, which) -> dialog.dismiss())) - .autoDismiss(false) - .show(); - } - - /* _____ _ _ _____ - / ____| | | |_ _| - | | __| | | | | | ___ _ __ _ __ ___ _ __ ___ - | | |_ | | | | | | / _ \ '__| '__/ _ \| '__/ __| - | |__| | |__| |_| |_ | __/ | | | | (_) | | \__ \ - \_____|\____/|_____| \___|_| |_| \___/|_| |__*/ - /** - * Used for reporting an exception somewhere in the code that is not part of Edziennik APIs. - * - * @param activity a parent activity - * @param errorLine the line of code where the error occurred - * @param e an Exception object - */ - public void guiReportException(Activity activity, int errorLine, Exception e) { - guiReportError(activity, new AppError(TAG, errorLine, CODE_OTHER, "Błąd wewnętrzny aplikacji ("+errorLine+")", null, null, e, null), null); - } - - public void guiShowErrorDialog(Activity activity, @NonNull AppError error, @StringRes int dialogTitle) { - if (error.errorCode == CODE_PROFILE_ARCHIVED) { - guiShowArchivedDialog(activity, error.errorText); - return; - } - error.changeIfCodeOther(); - new MaterialDialog.Builder(activity) - .title(dialogTitle) - .content(error.asReadableString(activity)) - .positiveText(R.string.ok) - .onPositive(((dialog, which) -> dialog.dismiss())) - .neutralText(R.string.sync_error_dialog_report_button) - .onNeutral(((dialog, which) -> { - guiReportError(activity, error, dialog); - })) - .autoDismiss(false) - .show(); - } - public void guiShowErrorSnackbar(MainActivity activity, @NonNull AppError error) { - if (error.errorCode == CODE_PROFILE_ARCHIVED) { - guiShowArchivedDialog(activity, error.errorText); - return; - } - - // TODO: 2019-08-28 - IconicsDrawable icon = new IconicsDrawable(activity) - .icon(CommunityMaterial.Icon.cmd_alert_circle); - sizeDp(icon, 20); - colorInt(icon, Themes.INSTANCE.getPrimaryTextColor(activity)); - - error.changeIfCodeOther(); - CafeBar.builder(activity) - .to(activity.findViewById(R.id.coordinator)) - .content(error.asReadableString(activity)) - .icon(icon) - .positiveText(R.string.more) - .positiveColor(0xff4caf50) - .negativeText(R.string.ok) - .negativeColor(0x66ffffff) - .onPositive((cafeBar -> guiReportError(activity, error, null))) - .onNegative((cafeBar -> cafeBar.dismiss())) - .autoDismiss(false) - .swipeToDismiss(true) - .floating(true) - .show(); - } - public void guiReportError(Activity activity, AppError error, @Nullable MaterialDialog parentDialogToDisableNeutral) { - String errorDetails = error.getDetails(activity); - String htmlErrorDetails = ""+errorDetails+""; - htmlErrorDetails = htmlErrorDetails.replaceAll(activity.getPackageName(), ""+activity.getPackageName()+""); - htmlErrorDetails = htmlErrorDetails.replaceAll("\n", "
"); - - new MaterialDialog.Builder(activity) - .title(R.string.sync_report_dialog_title) - .content(Html.fromHtml(htmlErrorDetails)) - .typeface(null, "RobotoMono-Regular.ttf") - .negativeText(R.string.close) - .onNegative(((dialog1, which1) -> dialog1.dismiss())) - .neutralText(R.string.copy_to_clipboard) - .onNeutral((dialog1, which1) -> { - ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(CLIPBOARD_SERVICE); - if (clipboard != null) { - ClipData clip = ClipData.newPlainText("Error report", errorDetails); - clipboard.setPrimaryClip(clip); - Toast.makeText(activity, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - }) - .autoDismiss(false) - .positiveText(R.string.sync_report_dialog_button) - .checkBoxPromptRes(R.string.sync_report_dialog_include_api_response, true, null) - .onPositive(((dialog1, which1) -> AsyncTask.execute(() -> error.getApiResponse(activity, apiResponse -> { - new ServerRequest(app, app.requestScheme + APP_URL + "main.php?report", "Edziennik/Report") - .setBodyParameter("base64_encoded", Base64.encodeToString(errorDetails.getBytes(), Base64.DEFAULT)) - .setBodyParameter("api_response", dialog1.isPromptCheckBoxChecked() ? Base64.encodeToString(apiResponse.getBytes(), Base64.DEFAULT) : "VW5jaGVja2Vk"/*Unchecked*/) - .run((e, result) -> { - new Handler(activity.getMainLooper()).post(() -> { - if (result != null) - { - if (result.get("success").getAsBoolean()) { - Toast.makeText(activity, activity.getString(R.string.crash_report_sent), Toast.LENGTH_SHORT).show(); - dialog1.getActionButton(DialogAction.POSITIVE).setEnabled(false); - if (parentDialogToDisableNeutral != null) - parentDialogToDisableNeutral.getActionButton(DialogAction.NEUTRAL).setEnabled(false); - } - else { - Toast.makeText(activity, activity.getString(R.string.crash_report_cannot_send) + ": " + result.get("reason").getAsString(), Toast.LENGTH_LONG).show(); - } - } - else - { - Toast.makeText(activity, activity.getString(R.string.crash_report_cannot_send)+" brak internetu", Toast.LENGTH_LONG).show(); - } - }); - }); - })))) - .show(); - } - - /** - * A method that displays a dialog allowing the user to report an error that has occurred. - * - * @param activity a parent activity - * @param errorCode self-explanatory - * @param errorText additional error information, that replaces text based on {@code errorCode} if it's {@code CODE_OTHER} - * @param throwable a {@link Throwable} containing the error details - * @param apiResponse response of the Edziennik API - * @param parentDialogToDisableNeutral if not null, an instance of {@link MaterialDialog} in which the neutral button should be disabled after submitting an error report - */ - public void guiReportError(Activity activity, int errorCode, String errorText, Throwable throwable, String apiResponse, @Nullable MaterialDialog parentDialogToDisableNeutral) { - // build a string containing the stack trace and the device name + user's registration data - String contentPlain = "Application Internal Error "+stringErrorType(errorCode)+":\n"+stringErrorCode(activity, errorCode, "")+"\n"+errorText+"\n\n"; - contentPlain += Log.getStackTraceString(throwable); - String content = ""+contentPlain+""; - content = content.replaceAll(activity.getPackageName(), ""+activity.getPackageName()+""); - content = content.replaceAll("\n", "
"); - - contentPlain += "\n"+Build.MANUFACTURER+"\n"+Build.BRAND+"\n"+Build.MODEL+"\n"+Build.DEVICE+"\n"; - if (app.profile != null && app.profile.getRegistration() == REGISTRATION_ENABLED) { - contentPlain += "U: "+app.profile.getUsernameId()+"\nS: "+ app.profile.getStudentNameLong() +"\nT: "+app.profile.loginStoreType()+"\n"; - } - contentPlain += BuildConfig.VERSION_NAME+" "+BuildConfig.BUILD_TYPE+"\nAndroid "+Build.VERSION.RELEASE; - - d(TAG, contentPlain); - d(TAG, apiResponse == null ? "API Response = null" : apiResponse); - - - // show a dialog containing the error details in HTML - String finalContentPlain = contentPlain; - new MaterialDialog.Builder(activity) - .title(R.string.sync_report_dialog_title) - .content(Html.fromHtml(content)) - .typeface(null, "RobotoMono-Regular.ttf") - .negativeText(R.string.close) - .onNegative(((dialog1, which1) -> dialog1.dismiss())) - .neutralText(R.string.copy_to_clipboard) - .onNeutral((dialog1, which1) -> { - ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(CLIPBOARD_SERVICE); - if (clipboard != null) { - ClipData clip = ClipData.newPlainText("Error report", finalContentPlain); - clipboard.setPrimaryClip(clip); - Toast.makeText(activity, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - }) - .autoDismiss(false) - .positiveText(R.string.sync_report_dialog_button) - .checkBoxPromptRes(R.string.sync_report_dialog_include_api_response, true, null) - .onPositive(((dialog1, which1) -> { - // send the error report - new ServerRequest(app, app.requestScheme + APP_URL + "main.php?report", "Edziennik/Report") - .setBodyParameter("base64_encoded", Base64.encodeToString(finalContentPlain.getBytes(), Base64.DEFAULT)) - .setBodyParameter("api_response", dialog1.isPromptCheckBoxChecked() ? apiResponse == null ? Base64.encodeToString("NULL XD".getBytes(), Base64.DEFAULT) : Base64.encodeToString(apiResponse.getBytes(), Base64.DEFAULT) : "VW5jaGVja2Vk"/*Unchecked*/) - .run((e, result) -> { - new Handler(Looper.getMainLooper()).post(() -> { - if (result != null) - { - if (result.get("success").getAsBoolean()) { - Toast.makeText(activity, activity.getString(R.string.crash_report_sent), Toast.LENGTH_SHORT).show(); - dialog1.getActionButton(DialogAction.POSITIVE).setEnabled(false); - if (parentDialogToDisableNeutral != null) - parentDialogToDisableNeutral.getActionButton(DialogAction.NEUTRAL).setEnabled(false); - } - else { - Toast.makeText(activity, activity.getString(R.string.crash_report_cannot_send) + ": " + result.get("reason").getAsString(), Toast.LENGTH_LONG).show(); - } - } - else - { - Toast.makeText(activity, activity.getString(R.string.crash_report_cannot_send)+" JsonObject equals null", Toast.LENGTH_LONG).show(); - } - }); - }); - })) - .show(); - } - - /* _____ __ _ _ _ - | __ \ / _(_) | | | - | |__) | __ ___ | |_ _| | ___ _ __ ___ _ __ ___ _____ ____ _| | - | ___/ '__/ _ \| _| | |/ _ \ | '__/ _ \ '_ ` _ \ / _ \ \ / / _` | | - | | | | | (_) | | | | | __/ | | | __/ | | | | | (_) \ V / (_| | | - |_| |_| \___/|_| |_|_|\___| |_| \___|_| |_| |_|\___/ \_/ \__,_|*/ - public void guiRemoveProfile(MainActivity activity, int profileId, String profileName) { - new MaterialDialog.Builder(activity) - .title(R.string.profile_menu_remove_confirm) - .content(activity.getString(R.string.profile_menu_remove_confirm_text_format, profileName, profileName)) - .positiveText(R.string.remove) - .negativeText(R.string.cancel) - .onPositive(((dialog, which) -> { - AsyncTask.execute(() -> { - removeProfile(profileId); - activity.runOnUiThread(() -> { - //activity.drawer.loadItem(DRAWER_ITEM_HOME, null, "ProfileRemoving"); - //activity.recreate(DRAWER_ITEM_HOME); - activity.reloadTarget(); - Toast.makeText(activity, "Profil został usunięty.", Toast.LENGTH_LONG).show(); - }); - }); - })) - .show(); - } - public void removeProfile(int profileId) { - Profile profileObject = app.db.profileDao().getByIdNow(profileId); - if (profileObject == null) - return; - app.db.announcementDao().clear(profileId); - app.db.attendanceDao().clear(profileId); - app.db.eventDao().clear(profileId); - app.db.eventTypeDao().clear(profileId); - app.db.gradeDao().clear(profileId); - app.db.gradeCategoryDao().clear(profileId); - app.db.lessonDao().clear(profileId); - app.db.lessonChangeDao().clear(profileId); - app.db.luckyNumberDao().clear(profileId); - app.db.noticeDao().clear(profileId); - app.db.subjectDao().clear(profileId); - app.db.teacherDao().clear(profileId); - app.db.teamDao().clear(profileId); - app.db.messageRecipientDao().clear(profileId); - app.db.messageDao().clear(profileId); - - int loginStoreId = profileObject.getLoginStoreId(); - List profilesUsingLoginStore = app.db.profileDao().getIdsByLoginStoreIdNow(loginStoreId); - if (profilesUsingLoginStore.size() == 1) { - app.db.loginStoreDao().remove(loginStoreId); - } - app.db.profileDao().remove(profileId); - app.db.metadataDao().deleteAll(profileId); - - List toRemove = new ArrayList<>(); - for (Notification notification: app.appConfig.notifications) { - if (notification.profileId == profileId) { - toRemove.add(notification); - } - } - app.appConfig.notifications.removeAll(toRemove); - - app.profile = null; - App.profileId = -1; - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt new file mode 100644 index 00000000..a78f8ab1 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt @@ -0,0 +1,140 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-1. + */ + +package pl.szczodrzynski.edziennik.data.api + +import android.app.Notification +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationCompat.PRIORITY_MIN +import pl.szczodrzynski.edziennik.R +import kotlin.math.roundToInt + + +class EdziennikNotification(val context: Context) { + companion object { + const val NOTIFICATION_ID = 20191001 + } + + private val notificationManager by lazy { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } + + private val notificationBuilder: NotificationCompat.Builder by lazy { + NotificationCompat.Builder(context, ApiService.NOTIFICATION_API_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setPriority(PRIORITY_MIN) + .setOngoing(true) + .setLocalOnly(true) + } + + val notification: Notification + get() = notificationBuilder.build() + + private var errorCount = 0 + private var criticalErrorCount = 0 + + private fun cancelPendingIntent(taskId: Int): PendingIntent { + val intent = Intent("pl.szczodrzynski.edziennik.SZKOLNY_MAIN") + intent.putExtra("task", "TaskCancelRequest") + intent.putExtra("taskId", taskId) + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) as PendingIntent + } + private val closePendingIntent: PendingIntent + get() { + val intent = Intent("pl.szczodrzynski.edziennik.SZKOLNY_MAIN") + intent.putExtra("task", "ServiceCloseRequest") + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) as PendingIntent + } + + private fun errorCountText(): String? { + var result = "" + if (criticalErrorCount > 0) { + result += context.resources.getQuantityString(R.plurals.critical_errors_format, criticalErrorCount, criticalErrorCount) + } + if (criticalErrorCount > 0 && errorCount > 0) { + result += ", " + } + if (errorCount > 0) { + result += context.resources.getQuantityString(R.plurals.normal_errors_format, errorCount, errorCount) + } + return if (result.isEmpty()) null else result + } + + fun setIdle(): EdziennikNotification { + notificationBuilder.setContentTitle(context.getString(R.string.edziennik_notification_api_title)) + notificationBuilder.setProgress(0, 0, false) + notificationBuilder.apply { + val str = context.getString(R.string.edziennik_notification_api_text) + setStyle(NotificationCompat.BigTextStyle().bigText(str)) + setContentText(str) + } + setCloseAction() + return this + } + + fun addError(): EdziennikNotification { + errorCount++ + return this + } + fun setCriticalError(): EdziennikNotification { + criticalErrorCount++ + notificationBuilder.setContentTitle(context.getString(R.string.edziennik_notification_api_error_title)) + notificationBuilder.setProgress(0, 0, false) + notificationBuilder.apply { + val str = errorCountText() + setStyle(NotificationCompat.BigTextStyle().bigText(str)) + setContentText(str) + } + setCloseAction() + return this + } + + fun setProgress(progress: Float): EdziennikNotification { + notificationBuilder.setProgress(100, progress.roundToInt(), progress < 0f) + return this + } + fun setProgressText(progressText: String?): EdziennikNotification { + notificationBuilder.setContentTitle(progressText) + return this + } + + fun setCurrentTask(taskId: Int, progressText: String?): EdziennikNotification { + notificationBuilder.setProgress(100, 0, true) + notificationBuilder.setContentTitle(progressText) + notificationBuilder.apply { + val str = errorCountText() + setStyle(NotificationCompat.BigTextStyle().bigText(str)) + setContentText(str) + } + setCancelAction(taskId) + return this + } + + fun setCloseAction(): EdziennikNotification { + notificationBuilder.mActions.clear() + notificationBuilder.addAction( + NotificationCompat.Action( + R.drawable.ic_notification, + context.getString(R.string.edziennik_notification_api_close), + closePendingIntent + )) + return this + } + private fun setCancelAction(taskId: Int) { + notificationBuilder.mActions.clear() + notificationBuilder.addAction( + NotificationCompat.Action( + R.drawable.ic_notification, + context.getString(R.string.edziennik_notification_api_cancel), + cancelPendingIntent(taskId) + )) + } + + fun post() { + notificationManager.notify(NOTIFICATION_ID, notification) + } + +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EndpointChooser.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EndpointChooser.kt new file mode 100644 index 00000000..9229950a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EndpointChooser.kt @@ -0,0 +1,112 @@ +package pl.szczodrzynski.edziennik.data.api + +import pl.szczodrzynski.edziennik.data.api.models.Data +import pl.szczodrzynski.edziennik.data.api.models.Feature +import pl.szczodrzynski.edziennik.data.api.models.LoginMethod +import pl.szczodrzynski.edziennik.data.db.modules.api.EndpointTimer +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_NEVER + +fun Data.prepare(loginMethods: List, features: List, featureIds: List, viewId: Int?) { + val data = this + + val possibleLoginMethods = data.loginMethods.toMutableList() + + for (loginMethod in loginMethods) { + if (loginMethod.isPossible(profile, loginStore)) + possibleLoginMethods += loginMethod.loginMethodId + } + + //var highestLoginMethod = 0 + var endpointList = mutableListOf() + val requiredLoginMethods = mutableListOf() + + data.targetEndpointIds.clear() + data.targetLoginMethodIds.clear() + + // get all endpoints for every feature, only if possible to login and possible/necessary to sync + for (featureId in featureIds) { + features.filter { + it.featureId == featureId // feature ID matches + && possibleLoginMethods.containsAll(it.requiredLoginMethods) // is possible to login + && it.shouldSync?.invoke(data) ?: true // is necessary/possible to sync + }.let { + endpointList.addAll(it) + } + } + + val timestamp = System.currentTimeMillis() + + endpointList = endpointList + // sort the endpoint list by feature ID and priority + .sortedWith(compareBy(Feature::featureId, Feature::priority)) + // select only the most important endpoint for each feature + .distinctBy { it.featureId } + .toMutableList() + // add all endpoint IDs and required login methods, filtering using timers + .onEach { feature -> + feature.endpointIds.forEach { endpoint -> + (data.endpointTimers + .singleOrNull { it.endpointId == endpoint.first } ?: EndpointTimer(data.profile?.id ?: -1, endpoint.first)) + .let { timer -> + if (timer.nextSync == SYNC_ALWAYS || + (viewId != null && timer.viewId == viewId) || + (timer.nextSync != SYNC_NEVER && timer.nextSync < timestamp)) { + data.targetEndpointIds.add(endpoint.first) + requiredLoginMethods.add(endpoint.second) + } + } + } + } + + // check every login method for any dependencies + for (loginMethodId in requiredLoginMethods) { + var requiredLoginMethod: Int? = loginMethodId + while (requiredLoginMethod != LOGIN_METHOD_NOT_NEEDED) { + loginMethods.singleOrNull { it.loginMethodId == requiredLoginMethod }?.let { loginMethod -> + if (requiredLoginMethod != null) + data.targetLoginMethodIds.add(requiredLoginMethod!!) + requiredLoginMethod = loginMethod.requiredLoginMethod(data.profile, data.loginStore) + } + } + } + + // sort and distinct every login method and endpoint + data.targetLoginMethodIds = data.targetLoginMethodIds.toHashSet().toMutableList() + data.targetLoginMethodIds.sort() + + data.targetEndpointIds = data.targetEndpointIds.toHashSet().toMutableList() + data.targetEndpointIds.sort() + + progressCount = targetLoginMethodIds.size + targetEndpointIds.size + progressStep = if (progressCount <= 0) 0f else 100f / progressCount.toFloat() +} + +fun Data.prepareFor(loginMethods: List, loginMethodId: Int) { + val possibleLoginMethods = this.loginMethods.toMutableList() + + loginMethods.forEach { + if (it.isPossible(profile, loginStore)) + possibleLoginMethods += it.loginMethodId + } + + targetEndpointIds.clear() + targetLoginMethodIds.clear() + + // check the login method for any dependencies + var requiredLoginMethod: Int? = loginMethodId + while (requiredLoginMethod != LOGIN_METHOD_NOT_NEEDED) { + loginMethods.singleOrNull { it.loginMethodId == requiredLoginMethod }?.let { + if (requiredLoginMethod != null) + targetLoginMethodIds.add(requiredLoginMethod!!) + requiredLoginMethod = it.requiredLoginMethod(profile, loginStore) + } + } + + // sort and distinct every login method + targetLoginMethodIds = targetLoginMethodIds.toHashSet().toMutableList() + targetLoginMethodIds.sort() + + progressCount = 0 + progressStep = 0f +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt new file mode 100644 index 00000000..2dff48ec --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt @@ -0,0 +1,196 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-21. + */ + +package pl.szczodrzynski.edziennik.data.api + +/*const val CODE_OTHER = 0 +const val CODE_OK = 1 +const val CODE_NO_INTERNET = 10 +const val CODE_SSL_ERROR = 13 +const val CODE_ARCHIVED = 5 +const val CODE_MAINTENANCE = 6 +const val CODE_LOGIN_ERROR = 7 +const val CODE_ACCOUNT_MISMATCH = 8 +const val CODE_APP_SERVER_ERROR = 9 +const val CODE_MULTIACCOUNT_SETUP = 12 +const val CODE_TIMEOUT = 11 +const val CODE_PROFILE_NOT_FOUND = 14 +const val CODE_ATTACHMENT_NOT_AVAILABLE = 28 +const val CODE_INVALID_LOGIN = 2 +const val CODE_INVALID_SERVER_ADDRESS = 21 +const val CODE_INVALID_SCHOOL_NAME = 22 +const val CODE_INVALID_DEVICE = 23 +const val CODE_OLD_PASSWORD = 4 +const val CODE_INVALID_TOKEN = 24 +const val CODE_EXPIRED_TOKEN = 27 +const val CODE_INVALID_SYMBOL = 25 +const val CODE_INVALID_PIN = 26 +const val CODE_LIBRUS_NOT_ACTIVATED = 29 +const val CODE_SYNERGIA_NOT_ACTIVATED = 32 +const val CODE_LIBRUS_DISCONNECTED = 31 +const val CODE_PROFILE_ARCHIVED = 30*/ + +const val ERROR_APP_CRASH = 1 +const val ERROR_MESSAGE_NOT_SENT = 10 + +const val ERROR_REQUEST_FAILURE = 50 +const val ERROR_REQUEST_HTTP_400 = 51 +const val ERROR_REQUEST_HTTP_401 = 52 +const val ERROR_REQUEST_HTTP_403 = 53 +const val ERROR_REQUEST_HTTP_404 = 54 +const val ERROR_REQUEST_HTTP_405 = 55 +const val ERROR_REQUEST_HTTP_410 = 56 +const val ERROR_REQUEST_HTTP_424 = 57 +const val ERROR_REQUEST_HTTP_500 = 58 +const val ERROR_REQUEST_HTTP_503 = 59 +const val ERROR_REQUEST_FAILURE_HOSTNAME_NOT_FOUND = 60 +const val ERROR_REQUEST_FAILURE_TIMEOUT = 61 +const val ERROR_REQUEST_FAILURE_NO_INTERNET = 62 +const val ERROR_REQUEST_FAILURE_SSL_ERROR = 63 +const val ERROR_RESPONSE_EMPTY = 100 +const val ERROR_LOGIN_DATA_MISSING = 101 +const val ERROR_PROFILE_MISSING = 105 +const val ERROR_INVALID_LOGIN_MODE = 110 +const val ERROR_LOGIN_METHOD_NOT_SATISFIED = 111 +const val ERROR_NOT_IMPLEMENTED = 112 +const val ERROR_FILE_DOWNLOAD = 113 + +const val ERROR_NO_STUDENTS_IN_ACCOUNT = 115 + +const val CODE_INTERNAL_LIBRUS_ACCOUNT_410 = 120 +const val CODE_INTERNAL_LIBRUS_SYNERGIA_EXPIRED = 121 +const val ERROR_LOGIN_LIBRUS_API_CAPTCHA_NEEDED = 124 +const val ERROR_LOGIN_LIBRUS_API_CONNECTION_PROBLEMS = 125 +const val ERROR_LOGIN_LIBRUS_API_INVALID_CLIENT = 126 +const val ERROR_LOGIN_LIBRUS_API_REG_ACCEPT_NEEDED = 127 +const val ERROR_LOGIN_LIBRUS_API_CHANGE_PASSWORD_ERROR = 128 +const val ERROR_LOGIN_LIBRUS_API_PASSWORD_CHANGE_REQUIRED = 129 +const val ERROR_LOGIN_LIBRUS_API_INVALID_LOGIN = 130 +const val ERROR_LOGIN_LIBRUS_API_OTHER = 131 +const val ERROR_LOGIN_LIBRUS_PORTAL_CSRF_MISSING = 132 +const val ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED = 133 +const val ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR = 134 +const val ERROR_LOGIN_LIBRUS_PORTAL_SYNERGIA_TOKEN_MISSING = 139 +const val ERROR_LIBRUS_API_TOKEN_EXPIRED = 140 +const val ERROR_LIBRUS_API_INSUFFICIENT_SCOPES = 141 +const val ERROR_LIBRUS_API_OTHER = 142 +const val ERROR_LIBRUS_API_ACCESS_DENIED = 143 +const val ERROR_LIBRUS_API_RESOURCE_NOT_FOUND = 144 +const val ERROR_LIBRUS_API_DATA_NOT_FOUND = 145 +const val ERROR_LIBRUS_API_TIMETABLE_NOT_PUBLIC = 146 +const val ERROR_LIBRUS_API_RESOURCE_ACCESS_DENIED = 147 +const val ERROR_LIBRUS_API_INVALID_REQUEST_PARAMS = 148 +const val ERROR_LIBRUS_API_INCORRECT_ENDPOINT = 149 +const val ERROR_LIBRUS_API_LUCKY_NUMBER_NOT_ACTIVE = 150 +const val ERROR_LIBRUS_API_NOTES_NOT_ACTIVE = 151 +const val ERROR_LOGIN_LIBRUS_SYNERGIA_NO_TOKEN = 152 +const val ERROR_LOGIN_LIBRUS_SYNERGIA_TOKEN_INVALID = 153 +const val ERROR_LOGIN_LIBRUS_SYNERGIA_NO_SESSION_ID = 154 +const val ERROR_LIBRUS_MESSAGES_ACCESS_DENIED = 155 +const val ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED = 156 +const val ERROR_LOGIN_LIBRUS_MESSAGES_NO_SESSION_ID = 157 +const val ERROR_LIBRUS_PORTAL_ACCESS_DENIED = 158 +const val ERROR_LIBRUS_PORTAL_API_DISABLED = 159 +const val ERROR_LIBRUS_PORTAL_SYNERGIA_DISCONNECTED = 160 +const val ERROR_LIBRUS_PORTAL_OTHER = 161 +const val ERROR_LIBRUS_PORTAL_SYNERGIA_NOT_FOUND = 162 +const val ERROR_LOGIN_LIBRUS_PORTAL_OTHER = 163 +const val ERROR_LOGIN_LIBRUS_PORTAL_CODE_EXPIRED = 164 +const val ERROR_LOGIN_LIBRUS_PORTAL_CODE_REVOKED = 165 +const val ERROR_LOGIN_LIBRUS_PORTAL_NO_CLIENT_ID = 166 +const val ERROR_LOGIN_LIBRUS_PORTAL_NO_CODE = 167 +const val ERROR_LOGIN_LIBRUS_PORTAL_NO_REFRESH = 168 +const val ERROR_LOGIN_LIBRUS_PORTAL_NO_REDIRECT = 169 +const val ERROR_LOGIN_LIBRUS_PORTAL_UNSUPPORTED_GRANT = 170 +const val ERROR_LOGIN_LIBRUS_PORTAL_INVALID_CLIENT_ID = 171 +const val ERROR_LOGIN_LIBRUS_PORTAL_REFRESH_INVALID = 172 +const val ERROR_LOGIN_LIBRUS_PORTAL_REFRESH_REVOKED = 173 +const val ERROR_LIBRUS_SYNERGIA_OTHER = 174 +const val ERROR_LIBRUS_SYNERGIA_MAINTENANCE = 175 +const val ERROR_LIBRUS_MESSAGES_MAINTENANCE = 176 +const val ERROR_LIBRUS_MESSAGES_ERROR = 177 +const val ERROR_LIBRUS_MESSAGES_OTHER = 178 +const val ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN = 179 +const val ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN = 180 +const val ERROR_LIBRUS_API_MAINTENANCE = 181 +const val ERROR_LIBRUS_PORTAL_MAINTENANCE = 182 + +const val ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN = 201 +const val ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD = 202 +const val ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_DEVICE = 203 +const val ERROR_LOGIN_MOBIDZIENNIK_WEB_ARCHIVED = 204 +const val ERROR_LOGIN_MOBIDZIENNIK_WEB_MAINTENANCE = 205 +const val ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_ADDRESS = 206 +const val ERROR_LOGIN_MOBIDZIENNIK_WEB_OTHER = 210 +const val ERROR_MOBIDZIENNIK_WEB_ACCESS_DENIED = 211 +const val ERROR_MOBIDZIENNIK_WEB_NO_SESSION_KEY = 212 +const val ERROR_MOBIDZIENNIK_WEB_NO_SESSION_VALUE = 216 +const val ERROR_MOBIDZIENNIK_WEB_NO_SERVER_ID = 213 +const val ERROR_MOBIDZIENNIK_WEB_INVALID_RESPONSE = 214 +const val ERROR_LOGIN_MOBIDZIENNIK_WEB_NO_SESSION_ID = 215 + +const val ERROR_LOGIN_VULCAN_INVALID_SYMBOL = 301 +const val ERROR_LOGIN_VULCAN_INVALID_TOKEN = 302 +const val ERROR_LOGIN_VULCAN_INVALID_PIN = 309 +const val ERROR_LOGIN_VULCAN_INVALID_PIN_0_REMAINING = 310 +const val ERROR_LOGIN_VULCAN_INVALID_PIN_1_REMAINING = 311 +const val ERROR_LOGIN_VULCAN_INVALID_PIN_2_REMAINING = 312 +const val ERROR_LOGIN_VULCAN_EXPIRED_TOKEN = 321 +const val ERROR_LOGIN_VULCAN_OTHER = 322 +const val ERROR_LOGIN_VULCAN_ONLY_KINDERGARTEN = 330 +const val ERROR_LOGIN_VULCAN_NO_PUPILS = 331 +const val ERROR_VULCAN_API_MAINTENANCE = 340 +const val ERROR_VULCAN_API_BAD_REQUEST = 341 +const val ERROR_VULCAN_API_OTHER = 342 + +const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN = 401 +const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME = 402 +const val ERROR_LOGIN_IDZIENNIK_WEB_PASSWORD_CHANGE_NEEDED = 403 +const val ERROR_LOGIN_IDZIENNIK_WEB_MAINTENANCE = 404 +const val ERROR_LOGIN_IDZIENNIK_WEB_SERVER_ERROR = 405 +const val ERROR_LOGIN_IDZIENNIK_WEB_OTHER = 410 +const val ERROR_LOGIN_IDZIENNIK_WEB_API_NO_ACCESS = 411 /* {"d":{"__type":"mds.Web.mod_komunikator.WS_mod_wiadomosci+detailWiadomosci","Wiadomosc":{"_recordId":0,"DataNadania":null,"DataOdczytania":null,"Nadawca":null,"ListaOdbiorcow":[],"Tytul":null,"Text":null,"ListaZal":[]},"Bledy":{"__type":"mds.Module.Globalne+sBledy","CzyJestBlad":true,"ListaBledow":["Nie masz dostępu do tych zasobów!"],"ListaKodowBledow":[]},"czyJestWiecej":false}} */ +const val ERROR_LOGIN_IDZIENNIK_WEB_NO_SESSION = 420 +const val ERROR_LOGIN_IDZIENNIK_WEB_NO_AUTH = 421 +const val ERROR_LOGIN_IDZIENNIK_WEB_NO_BEARER = 422 +const val ERROR_IDZIENNIK_WEB_ACCESS_DENIED = 430 +const val ERROR_IDZIENNIK_WEB_OTHER = 431 +const val ERROR_IDZIENNIK_WEB_MAINTENANCE = 432 +const val ERROR_IDZIENNIK_WEB_SERVER_ERROR = 433 +const val ERROR_IDZIENNIK_WEB_PASSWORD_CHANGE_NEEDED = 434 +const val ERROR_LOGIN_IDZIENNIK_FIRST_NO_SCHOOL_YEAR = 440 +const val ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA = 441 +const val ERROR_IDZIENNIK_API_ACCESS_DENIED = 450 +const val ERROR_IDZIENNIK_API_OTHER = 451 +const val ERROR_IDZIENNIK_API_NO_REGISTER = 452 + +const val ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN = 501 +const val ERROR_LOGIN_EDUDZIENNIK_WEB_OTHER = 510 +const val ERROR_LOGIN_EDUDZIENNIK_WEB_NO_SESSION_ID = 511 +const val ERROR_EDUDZIENNIK_WEB_TIMETABLE_NOT_PUBLIC = 520 +const val ERROR_EDUDZIENNIK_WEB_LIMITED_ACCESS = 521 +const val ERROR_EDUDZIENNIK_WEB_SESSION_EXPIRED = 522 +const val ERROR_EDUDZIENNIK_WEB_TEAM_MISSING = 530 + +const val ERROR_TEMPLATE_WEB_OTHER = 801 + +const val EXCEPTION_API_TASK = 900 +const val EXCEPTION_LOGIN_LIBRUS_API_TOKEN = 901 +const val EXCEPTION_LOGIN_LIBRUS_PORTAL_TOKEN = 902 +const val EXCEPTION_LIBRUS_PORTAL_SYNERGIA_TOKEN = 903 +const val EXCEPTION_LIBRUS_API_REQUEST = 904 +const val EXCEPTION_LIBRUS_SYNERGIA_REQUEST = 905 +const val EXCEPTION_MOBIDZIENNIK_WEB_REQUEST = 906 +const val EXCEPTION_VULCAN_API_REQUEST = 907 +const val EXCEPTION_MOBIDZIENNIK_WEB_FILE_REQUEST = 908 +const val EXCEPTION_LIBRUS_MESSAGES_FILE_REQUEST = 909 +const val EXCEPTION_NOTIFY = 910 +const val EXCEPTION_LIBRUS_MESSAGES_REQUEST = 911 +const val EXCEPTION_IDZIENNIK_WEB_REQUEST = 912 +const val EXCEPTION_IDZIENNIK_WEB_API_REQUEST = 913 +const val EXCEPTION_IDZIENNIK_API_REQUEST = 914 +const val EXCEPTION_EDUDZIENNIK_WEB_REQUEST = 920 +const val EXCEPTION_EDUDZIENNIK_FILE_REQUEST = 921 + +const val LOGIN_NO_ARGUMENTS = 1201 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Features.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Features.kt new file mode 100644 index 00000000..40f24048 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Features.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-29. + */ + +package pl.szczodrzynski.edziennik.data.api + +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_AGENDA +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ANNOUNCEMENTS +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ATTENDANCE +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_BEHAVIOUR +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_GRADES +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOME +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOMEWORK +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT + +internal const val FEATURE_TIMETABLE = 1 +internal const val FEATURE_AGENDA = 2 +internal const val FEATURE_GRADES = 3 +internal const val FEATURE_HOMEWORK = 4 +internal const val FEATURE_BEHAVIOUR = 5 +internal const val FEATURE_ATTENDANCE = 6 +internal const val FEATURE_MESSAGES_INBOX = 7 +internal const val FEATURE_MESSAGES_SENT = 8 +internal const val FEATURE_ANNOUNCEMENTS = 9 + +internal const val FEATURE_ALWAYS_NEEDED = 100 +internal const val FEATURE_STUDENT_INFO = 101 +internal const val FEATURE_STUDENT_NUMBER = 109 +internal const val FEATURE_SCHOOL_INFO = 102 +internal const val FEATURE_CLASS_INFO = 103 +internal const val FEATURE_TEAM_INFO = 104 +internal const val FEATURE_LUCKY_NUMBER = 105 +internal const val FEATURE_TEACHERS = 106 +internal const val FEATURE_SUBJECTS = 107 +internal const val FEATURE_CLASSROOMS = 108 +internal const val FEATURE_PUSH_CONFIG = 120 + +object Features { + private fun getAllNecessary(): List = listOf( + FEATURE_ALWAYS_NEEDED, + FEATURE_STUDENT_INFO, + FEATURE_STUDENT_NUMBER, + FEATURE_SCHOOL_INFO, + FEATURE_CLASS_INFO, + FEATURE_TEAM_INFO, + FEATURE_LUCKY_NUMBER, + FEATURE_TEACHERS, + FEATURE_SUBJECTS, + FEATURE_CLASSROOMS) + + private fun getAllFeatures(): List = listOf( + FEATURE_TIMETABLE, + FEATURE_AGENDA, + FEATURE_GRADES, + FEATURE_HOMEWORK, + FEATURE_BEHAVIOUR, + FEATURE_ATTENDANCE, + FEATURE_MESSAGES_INBOX, + FEATURE_MESSAGES_SENT, + FEATURE_ANNOUNCEMENTS) + + fun getAllIds(): List = getAllFeatures() + getAllNecessary() + + fun getIdsByView(targetId: Int, targetType: Int): List { + return (when (targetId) { + DRAWER_ITEM_HOME -> getAllFeatures() + DRAWER_ITEM_TIMETABLE -> listOf(FEATURE_TIMETABLE) + DRAWER_ITEM_AGENDA -> listOf(FEATURE_AGENDA) + DRAWER_ITEM_GRADES -> listOf(FEATURE_GRADES) + DRAWER_ITEM_MESSAGES -> when (targetType) { + TYPE_RECEIVED -> listOf(FEATURE_MESSAGES_INBOX) + TYPE_SENT -> listOf(FEATURE_MESSAGES_SENT) + else -> listOf(FEATURE_MESSAGES_INBOX, FEATURE_MESSAGES_SENT) + } + DRAWER_ITEM_HOMEWORK -> listOf(FEATURE_HOMEWORK) + DRAWER_ITEM_BEHAVIOUR -> listOf(FEATURE_BEHAVIOUR) + DRAWER_ITEM_ATTENDANCE -> listOf(FEATURE_ATTENDANCE) + DRAWER_ITEM_ANNOUNCEMENTS -> listOf(FEATURE_ANNOUNCEMENTS) + else -> getAllFeatures() + } + getAllNecessary()).sorted() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Iuczniowie.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Iuczniowie.java deleted file mode 100644 index 33c69671..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Iuczniowie.java +++ /dev/null @@ -1,1710 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api; - -import android.content.Context; -import android.graphics.Color; -import android.os.AsyncTask; -import android.os.Handler; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.util.Pair; - -import com.crashlytics.android.Crashlytics; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonNull; -import com.google.gson.JsonObject; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import im.wangchao.mhttp.Request; -import im.wangchao.mhttp.Response; -import im.wangchao.mhttp.body.MediaTypeUtils; -import im.wangchao.mhttp.callback.JsonArrayCallbackHandler; -import im.wangchao.mhttp.callback.JsonCallbackHandler; -import im.wangchao.mhttp.callback.TextCallbackHandler; -import okhttp3.Cookie; -import okhttp3.HttpUrl; -import pl.szczodrzynski.edziennik.App; -import pl.szczodrzynski.edziennik.BuildConfig; -import pl.szczodrzynski.edziennik.R; -import pl.szczodrzynski.edziennik.data.api.interfaces.AttachmentGetCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface; -import pl.szczodrzynski.edziennik.data.api.interfaces.LoginCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.MessageGetCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.RecipientListGetCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.SyncCallback; -import pl.szczodrzynski.edziennik.data.db.modules.announcements.Announcement; -import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance; -import pl.szczodrzynski.edziennik.data.db.modules.events.Event; -import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade; -import pl.szczodrzynski.edziennik.data.db.modules.lessons.Lesson; -import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange; -import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore; -import pl.szczodrzynski.edziennik.data.db.modules.luckynumber.LuckyNumber; -import pl.szczodrzynski.edziennik.data.db.modules.messages.Message; -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull; -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient; -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipientFull; -import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata; -import pl.szczodrzynski.edziennik.data.db.modules.notices.Notice; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull; -import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject; -import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher; -import pl.szczodrzynski.edziennik.data.db.modules.teams.Team; -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesComposeInfo; -import pl.szczodrzynski.edziennik.utils.models.Date; -import pl.szczodrzynski.edziennik.utils.models.Endpoint; -import pl.szczodrzynski.edziennik.utils.models.Time; -import pl.szczodrzynski.edziennik.utils.models.Week; - -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_INVALID_LOGIN; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_INVALID_SCHOOL_NAME; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_MAINTENANCE; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_OTHER; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_ABSENT; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_ABSENT_EXCUSED; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_BELATED; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_PRESENT; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_RELEASED; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER1_FINAL; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER1_PROPOSED; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_YEAR_FINAL; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_YEAR_PROPOSED; -import static pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange.TYPE_CANCELLED; -import static pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange.TYPE_CHANGE; -import static pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_DELETED; -import static pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED; -import static pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT; -import static pl.szczodrzynski.edziennik.data.db.modules.notices.Notice.TYPE_NEGATIVE; -import static pl.szczodrzynski.edziennik.data.db.modules.notices.Notice.TYPE_NEUTRAL; -import static pl.szczodrzynski.edziennik.data.db.modules.notices.Notice.TYPE_POSITIVE; -import static pl.szczodrzynski.edziennik.utils.Utils.crc16; -import static pl.szczodrzynski.edziennik.utils.Utils.crc32; -import static pl.szczodrzynski.edziennik.utils.Utils.d; -import static pl.szczodrzynski.edziennik.utils.Utils.getWordGradeValue; - -public class Iuczniowie implements EdziennikInterface { - public Iuczniowie(App app) { - this.app = app; - } - - private static final String TAG = "api.Iuczniowie"; - private static String IDZIENNIK_URL = "https://iuczniowie.progman.pl/idziennik"; - private static final String userAgent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36"; - - private App app; - private Context activityContext = null; - private SyncCallback callback = null; - private int profileId = -1; - private Profile profile = null; - private LoginStore loginStore = null; - private boolean fullSync = true; - private Date today = Date.getToday(); - private List targetEndpoints = new ArrayList<>(); - - // PROGRESS - private static final int PROGRESS_LOGIN = 10; - private int PROGRESS_COUNT = 1; - private int PROGRESS_STEP = (90/PROGRESS_COUNT); - - private int onlyFeature = FEATURE_ALL; - - private List teamList; - private List teacherList; - private List subjectList; - private List lessonList; - private List lessonChangeList; - private List gradeList; - private List eventList; - private List noticeList; - private List attendanceList; - private List announcementList; - private List messageList; - private List messageRecipientList; - private List messageRecipientIgnoreList; - private List metadataList; - private List messageMetadataList; - - private static boolean fakeLogin = false; - private String lastLogin = ""; - private long lastLoginTime = -1; - private String lastResponse = null; - private String loginSchoolName = null; - private String loginUsername = null; - private String loginPassword = null; - private String loginBearerToken = null; - private int loginRegisterId = -1; - private int loginSchoolYearId = -1; - private String loginStudentId = null; - private int teamClassId = -1; - - private boolean prepare(@NonNull Context activityContext, @NonNull SyncCallback callback, int profileId, @Nullable Profile profile, @NonNull LoginStore loginStore) { - this.activityContext = activityContext; - this.callback = callback; - this.profileId = profileId; - // here we must have a login store: either with a correct ID or -1 - // there may be no profile and that's when onLoginFirst happens - this.profile = profile; - this.loginStore = loginStore; - this.fullSync = profile == null || profile.getEmpty() || profile.shouldFullSync(activityContext); - this.today = Date.getToday(); - - this.loginSchoolName = loginStore.getLoginData("schoolName", ""); - this.loginUsername = loginStore.getLoginData("username", ""); - this.loginPassword = loginStore.getLoginData("password", ""); - if (loginSchoolName.equals("") || loginUsername.equals("") || loginPassword.equals("")) { - finishWithError(new AppError(TAG, 162, CODE_INVALID_LOGIN, "Login field is empty")); - return false; - } - fakeLogin = BuildConfig.DEBUG && loginUsername.startsWith("FAKE"); - IDZIENNIK_URL = fakeLogin ? "http://szkolny.eu/idziennik" : "https://iuczniowie.progman.pl/idziennik"; - - teamList = profileId == -1 ? new ArrayList<>() : app.db.teamDao().getAllNow(profileId); - teacherList = profileId == -1 ? new ArrayList<>() : app.db.teacherDao().getAllNow(profileId); - subjectList = profileId == -1 ? new ArrayList<>() : app.db.subjectDao().getAllNow(profileId); - lessonList = new ArrayList<>(); - lessonChangeList = new ArrayList<>(); - gradeList = new ArrayList<>(); - eventList = new ArrayList<>(); - noticeList = new ArrayList<>(); - attendanceList = new ArrayList<>(); - announcementList = new ArrayList<>(); - messageList = new ArrayList<>(); - messageRecipientList = new ArrayList<>(); - messageRecipientIgnoreList = new ArrayList<>(); - metadataList = new ArrayList<>(); - messageMetadataList = new ArrayList<>(); - - return true; - } - - @Override - public void sync(@NonNull Context activityContext, @NonNull SyncCallback callback, int profileId, @Nullable Profile profile, @NonNull LoginStore loginStore) { - if (!prepare(activityContext, callback, profileId, profile, loginStore)) - return; - - login(() -> { - targetEndpoints = new ArrayList<>(); - targetEndpoints.add("LuckyNumberAndSemesterDates"); - targetEndpoints.add("Timetable"); - targetEndpoints.add("Grades"); - targetEndpoints.add("PropositionGrades"); - targetEndpoints.add("Exams"); - targetEndpoints.add("Notices"); - targetEndpoints.add("Announcements"); - targetEndpoints.add("Attendance"); - targetEndpoints.add("MessagesInbox"); - targetEndpoints.add("MessagesOutbox"); - targetEndpoints.add("Finish"); - PROGRESS_COUNT = targetEndpoints.size()-1; - PROGRESS_STEP = (90/PROGRESS_COUNT); - begin(); - }); - } - @Override - public void syncFeature(@NonNull Context activityContext, @NonNull SyncCallback callback, @NonNull ProfileFull profile, int ... featureList) { - if (featureList == null) { - sync(activityContext, callback, profile.getId(), profile, LoginStore.fromProfileFull(profile)); - return; - } - if (!prepare(activityContext, callback, profile.getId(), profile, LoginStore.fromProfileFull(profile))) - return; - - login(() -> { - targetEndpoints = new ArrayList<>(); - if (featureList.length == 1) - onlyFeature = featureList[0]; - targetEndpoints.add("LuckyNumberAndSemesterDates"); - for (int feature: featureList) { - switch (feature) { - case FEATURE_TIMETABLE: - targetEndpoints.add("Timetable"); - break; - case FEATURE_AGENDA: - targetEndpoints.add("Exams"); - break; - case FEATURE_GRADES: - targetEndpoints.add("Grades"); - targetEndpoints.add("PropositionGrades"); - break; - case FEATURE_HOMEWORK: - targetEndpoints.add("Homework"); - break; - case FEATURE_NOTICES: - targetEndpoints.add("Notices"); - break; - case FEATURE_ATTENDANCE: - targetEndpoints.add("Attendance"); - break; - case FEATURE_MESSAGES_INBOX: - targetEndpoints.add("MessagesInbox"); - break; - case FEATURE_MESSAGES_OUTBOX: - targetEndpoints.add("MessagesOutbox"); - break; - case FEATURE_ANNOUNCEMENTS: - targetEndpoints.add("Announcements"); - break; - } - } - targetEndpoints.add("Finish"); - PROGRESS_COUNT = targetEndpoints.size()-1; - PROGRESS_STEP = (90/PROGRESS_COUNT); - begin(); - }); - } - - private void begin() { - List cookieList = app.cookieJar.loadForRequest(HttpUrl.get(IDZIENNIK_URL)); - for (Cookie cookie: cookieList) { - if (cookie.name().equalsIgnoreCase("Bearer")) { - loginBearerToken = cookie.value(); - } - } - loginStudentId = profile.getStudentData("studentId", null); - loginSchoolYearId = profile.getStudentData("schoolYearId", -1); - loginRegisterId = profile.getStudentData("registerId", -1); - - if (loginRegisterId == -1) { - finishWithError(new AppError(TAG, 212, CODE_OTHER, app.getString(R.string.error_register_id_not_found), "loginRegisterId == -1")); - return; - } - if (loginSchoolYearId == -1) { - finishWithError(new AppError(TAG, 216, CODE_OTHER, app.getString(R.string.error_school_year_not_found), "loginSchoolYearId == -1")); - return; - } - if (loginStudentId == null) { - if (lastResponse == null) { - lastLoginTime = -1; - lastLogin = ""; - finishWithError(new AppError(TAG, 223, CODE_OTHER, app.getString(R.string.error_student_id_not_found), "loginStudentId == null && lastResponse == null")); - return; - } - Matcher selectMatcher = Pattern.compile("", Pattern.DOTALL).matcher(lastResponse); - if (!selectMatcher.find()) { - finishWithError(new AppError(TAG, 228, CODE_OTHER, app.getString(R.string.error_register_id_not_found), lastResponse)); - return; - } - Matcher idMatcher = Pattern.compile(".*?", Pattern.DOTALL).matcher(selectMatcher.group(0)); - while (idMatcher.find()) { - loginStudentId = idMatcher.group(1); - profile.putStudentData("studentId", loginStudentId); - } - } - - this.attendanceMonth = today.month; - this.attendanceYear = today.year; - this.attendancePrevMonthChecked = false; - this.examsMonth = today.month; - this.examsYear = today.year; - this.examsMonthsChecked = 0; - this.examsNextMonthChecked = false; - - callback.onProgress(PROGRESS_LOGIN); - - r("get", null); - } - - private void r(String type, String endpoint) { - // endpoint == null when beginning - if (endpoint == null) - endpoint = targetEndpoints.get(0); - int index = -1; - for (String request: targetEndpoints) { - index++; - if (request.equals(endpoint)) { - break; - } - } - if (type.equals("finish")) { - // called when finishing the action - callback.onProgress(PROGRESS_STEP); - index++; - } - d(TAG, "Called r("+type+", "+endpoint+"). Getting "+targetEndpoints.get(index)); - switch (targetEndpoints.get(index)) { - case "LuckyNumberAndSemesterDates": - getLuckyNumberAndSemesterDates(); - break; - case "Timetable": - getTimetable(); - break; - case "Grades": - getGrades(); - break; - case "PropositionGrades": - getPropositionGrades(); - break; - case "Exams": - getExams(); - break; - case "Notices": - getNotices(); - break; - case "Announcements": - getAnnouncements(); - break; - case "Attendance": - getAttendance(); - break; - case "MessagesInbox": - getMessagesInbox(); - break; - case "MessagesOutbox": - getMessagesOutbox(); - break; - case "Finish": - finish(); - break; - } - } - private void saveData() { - if (teamList.size() > 0) { - //app.db.teamDao().clear(profileId); - app.db.teamDao().addAll(teamList); - } - if (teacherList.size() > 0) - app.db.teacherDao().addAll(teacherList); - if (subjectList.size() > 0) - app.db.subjectDao().addAll(subjectList); - if (lessonList.size() > 0) { - app.db.lessonDao().clear(profileId); - app.db.lessonDao().addAll(lessonList); - } - if (lessonChangeList.size() > 0) - app.db.lessonChangeDao().addAll(lessonChangeList); - if (gradeList.size() > 0) { - app.db.gradeDao().clear(profileId); - app.db.gradeDao().addAll(gradeList); - } - if (eventList.size() > 0) { - app.db.eventDao().removeFuture(profileId, today); - app.db.eventDao().addAll(eventList); - } - if (noticeList.size() > 0) { - app.db.noticeDao().clear(profileId); - app.db.noticeDao().addAll(noticeList); - } - if (attendanceList.size() > 0) - app.db.attendanceDao().addAll(attendanceList); - if (announcementList.size() > 0) - app.db.announcementDao().addAll(announcementList); - if (messageList.size() > 0) - app.db.messageDao().addAllIgnore(messageList); - if (messageRecipientList.size() > 0) - app.db.messageRecipientDao().addAll(messageRecipientList); - if (messageRecipientIgnoreList.size() > 0) - app.db.messageRecipientDao().addAllIgnore(messageRecipientIgnoreList); - if (metadataList.size() > 0) - app.db.metadataDao().addAllIgnore(metadataList); - if (messageMetadataList.size() > 0) - app.db.metadataDao().setSeen(messageMetadataList); - } - private void finish() { - try { - saveData(); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 363, CODE_OTHER, app.getString(R.string.sync_error_saving_data), null, null, e, null)); - } - if (fullSync) { - profile.setLastFullSync(System.currentTimeMillis()); - fullSync = false; - } - profile.setEmpty(false); - callback.onSuccess(activityContext, new ProfileFull(profile, loginStore)); - } - private void finishWithError(AppError error) { - try { - saveData(); - } - catch (Exception e) { - Crashlytics.logException(e); - } - callback.onError(activityContext, error); - } - - /* _ _ - | | (_) - | | ___ __ _ _ _ __ - | | / _ \ / _` | | '_ \ - | |___| (_) | (_| | | | | | - |______\___/ \__, |_|_| |_| - __/ | - |__*/ - private void login(@NonNull LoginCallback loginCallback) { - if (lastLogin.equals(loginSchoolName +":"+ loginUsername) - && System.currentTimeMillis() - lastLoginTime < 5 * 60 * 1000 - && profile != null) { // less than 5 minutes, use the already logged in account - loginCallback.onSuccess(); - return; - } - app.cookieJar.clearForDomain("iuczniowie.progman.pl"); - callback.onActionStarted(R.string.sync_action_logging_in); - Request.builder() - .url(IDZIENNIK_URL +"/login.aspx") - .userAgent(userAgent) - .callback(new TextCallbackHandler() { - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 389, CODE_OTHER, response, throwable)); - } - - @Override - public void onSuccess(String data1, Response response1) { - if (data1 == null || data1.equals("")) { // for safety - finishWithError(new AppError(TAG, 395, CODE_MAINTENANCE, response1)); - return; - } - //Log.d(TAG, "r:"+data); - Request.Builder builder = Request.builder() - .url(IDZIENNIK_URL +"/login.aspx") - .userAgent(userAgent) - //.withClient(app.httpLazy) - .addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/png,*/*;q=0.8") - .addHeader("Cache-Control", "max-age=0") - .addHeader("Origin", "https://iuczniowie.progman.pl") - .addHeader("Referer", "https://iuczniowie.progman.pl/idziennik/login.aspx") - .addHeader("Upgrade-Insecure-Requests", "1") - .contentType(MediaTypeUtils.APPLICATION_FORM) - .addParameter("ctl00$ContentPlaceHolder$nazwaPrzegladarki", userAgent) - .addParameter("ctl00$ContentPlaceHolder$NazwaSzkoly", loginSchoolName) - .addParameter("ctl00$ContentPlaceHolder$UserName", loginUsername) - .addParameter("ctl00$ContentPlaceHolder$Password", loginPassword) - .addParameter("ctl00$ContentPlaceHolder$captcha", "") - .addParameter("ctl00$ContentPlaceHolder$Logowanie", "Zaloguj") - .post(); - - // extract hidden form fields __VIEWSTATE __VIEWSTATEGENERATOR __EVENTVALIDATION - //Pattern pattern = Pattern.compile("<.+?name=\"__VIEWSTATE\".+?value=\"([A-z0-9+/=]+)\".+?name=\"__VIEWSTATEGENERATOR\".+?value=\"([A-z0-9+/=]+)\".+?name=\"__EVENTVALIDATION\".+?value=\"([A-z0-9+/=]+)\".+?>", Pattern.DOTALL); - //Pattern pattern = Pattern.compile("]* name=[\"']([^'\"]*)|)(?=[^>]* value=[\"']([^'\"]*)|)", Pattern.DOTALL); - Pattern pattern = Pattern.compile("", Pattern.DOTALL); - Matcher matcher = pattern.matcher(data1); - while (matcher.find()) { - //Log.d(TAG, "Match: "+matcher.group(1)+"="+matcher.group(2)); - builder.addParameter(matcher.group(1), matcher.group(2)); - } - - builder.callback(new TextCallbackHandler() { - @Override - public void onSuccess(String data2, Response response2) { - callback.onProgress(PROGRESS_LOGIN); - Pattern errorPattern = Pattern.compile("id=\"spanErrorMessage\">(.*?)", Pattern.DOTALL).matcher(data2); - if (!selectMatcher.find()) { - finishWithError(new AppError(TAG, 473, CODE_OTHER, app.getString(R.string.error_register_id_not_found), response2, data2)); - return; - } - Log.d(TAG, "g" + selectMatcher.group(0)); - Matcher idMatcher = Pattern.compile("(.+?)\\s(.+?)\\s*\\((.+?),\\s*(.+?)\\)", Pattern.DOTALL).matcher(selectMatcher.group(0)); - while (idMatcher.find()) { - if (loginRegisterId != Integer.parseInt(idMatcher.group(1))) - continue; - String teamClassName = idMatcher.group(4) + " " + idMatcher.group(5); - teamClassId = crc16(teamClassName.getBytes()); - app.db.teamDao().add(new Team( - profileId, - teamClassId, - teamClassName, - 1, - loginSchoolName+":"+teamClassName, - -1 - )); - } - loginCallback.onSuccess(); - return; - } - try { - Matcher yearMatcher = Pattern.compile("name=\"ctl00\\$dxComboRokSzkolny\".+?selected=\"selected\".*?value=\"([0-9]+)\"", Pattern.DOTALL).matcher(data2); - if (yearMatcher.find()) { - try { - loginSchoolYearId = Integer.parseInt(yearMatcher.group(1)); - } catch (Exception ex) { - finishWithError(new AppError(TAG, 501, CODE_OTHER, response2, ex, data2)); - return; - } - } else { - if (data2.contains("Hasło dostępu do systemu wygasło")) { - finishWithError(new AppError(TAG, 504, CODE_OTHER, app.getString(R.string.error_must_change_password), response2, data2)); - return; - } - finishWithError(new AppError(TAG, 507, CODE_OTHER, app.getString(R.string.error_school_year_not_found), response2, data2)); - return; - } - - List studentIds = new ArrayList<>(); - List registerIds = new ArrayList<>(); - List studentNamesLong = new ArrayList<>(); - List studentNamesShort = new ArrayList<>(); - List studentTeams = new ArrayList<>(); - - Matcher selectMatcher = Pattern.compile("", Pattern.DOTALL).matcher(data2); - if (!selectMatcher.find()) { - finishWithError(new AppError(TAG, 519, CODE_OTHER, app.getString(R.string.error_register_id_not_found), response2, data2)); - return; - } - Log.d(TAG, "g" + selectMatcher.group(0)); - Matcher idMatcher = Pattern.compile("(.+?)\\s(.+?)\\s*\\((.+?),\\s*(.+?)\\)", Pattern.DOTALL).matcher(selectMatcher.group(0)); - while (idMatcher.find()) { - registerIds.add(Integer.parseInt(idMatcher.group(1))); - String studentId = idMatcher.group(2); - String studentFirstName = idMatcher.group(3); - String studentLastName = idMatcher.group(4); - String teamClassName = idMatcher.group(5) + " " + idMatcher.group(6); - studentIds.add(studentId); - studentNamesLong.add(studentFirstName + " " + studentLastName); - studentNamesShort.add(studentFirstName + " " + studentLastName.charAt(0) + "."); - studentTeams.add(teamClassName); - } - Collections.reverse(studentIds); - Collections.reverse(registerIds); - Collections.reverse(studentNamesLong); - Collections.reverse(studentNamesShort); - Collections.reverse(studentTeams); - - List profileList = new ArrayList<>(); - for (int index = 0; index < registerIds.size(); index++) { - Profile newProfile = new Profile(); - newProfile.setStudentNameLong(studentNamesLong.get(index)); - newProfile.setStudentNameShort(studentNamesShort.get(index)); - newProfile.setName(newProfile.getStudentNameLong()); - newProfile.setSubname(loginUsername); - newProfile.setEmpty(true); - newProfile.setLoggedIn(true); - newProfile.putStudentData("studentId", studentIds.get(index)); - newProfile.putStudentData("registerId", registerIds.get(index)); - newProfile.putStudentData("schoolYearId", loginSchoolYearId); - profileList.add(newProfile); - } - - callback.onLoginFirst(profileList, loginStore); - } catch (Exception ex) { - finishWithError(new AppError(TAG, 557, CODE_OTHER, response2, ex, data2)); - } - } - - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 563, CODE_OTHER, response, throwable, data1)); - } - }).build().enqueue(); - } - }).build().enqueue(); - } - - /* _ _ _ _ _ _ _ - | | | | | | ___ | | | | | | - | |__| | ___| |_ __ ___ _ __ ___ ( _ ) ___ __ _| | | |__ __ _ ___| | _____ - | __ |/ _ \ | '_ \ / _ \ '__/ __| / _ \/\ / __/ _` | | | '_ \ / _` |/ __| |/ / __| - | | | | __/ | |_) | __/ | \__ \ | (_> < | (_| (_| | | | |_) | (_| | (__| <\__ \ - |_| |_|\___|_| .__/ \___|_| |___/ \___/\/ \___\__,_|_|_|_.__/ \__,_|\___|_|\_\___/ - | | - |*/ - private interface ApiRequestCallback { - void onSuccess(JsonObject result, Response response); - } - private void apiRequest(Request.Builder requestBuilder, ApiRequestCallback apiRequestCallback) { - requestBuilder.callback(new JsonCallbackHandler() { - @Override - public void onSuccess(JsonObject data, Response response) { - if (data == null) { - finishWithError(new AppError(TAG, 578, CODE_MAINTENANCE, response)); - return; - } - try { - apiRequestCallback.onSuccess(data, response); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 583, CODE_OTHER, response, e, data)); - } - } - - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 592, CODE_OTHER, response, throwable)); - } - }) - .build() - .enqueue(); - } - private interface ApiRequestArrayCallback { - void onSuccess(JsonArray result, Response response); - } - private void apiRequestArray(Request.Builder requestBuilder, ApiRequestArrayCallback apiRequestArrayCallback) { - requestBuilder.callback(new JsonArrayCallbackHandler() { - @Override - public void onSuccess(JsonArray data, Response response) { - if (data == null) { - finishWithError(new AppError(TAG, 603, CODE_MAINTENANCE, response)); - return; - } - try { - apiRequestArrayCallback.onSuccess(data, response); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 610, CODE_OTHER, response, e, data.toString())); - } - } - - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 616, CODE_OTHER, response, throwable)); - } - }) - .build() - .enqueue(); - } - - private Subject searchSubject(String name, long id, String shortName) { - Subject subject; - if (id == -1) - subject = Subject.getByName(subjectList, name); - else - subject = Subject.getById(subjectList, id); - - if (subject == null) { - subject = new Subject(profileId, (id == -1 ? crc16(name.getBytes()) : id), name, shortName); - subjectList.add(subject); - } - return subject; - } - - private Teacher searchTeacher(String firstName, String lastName) { - Teacher teacher = Teacher.getByFullName(teacherList, firstName+" "+lastName); - return validateTeacher(teacher, firstName, lastName); - } - private Teacher searchTeacher(char firstNameChar, String lastName) { - Teacher teacher = Teacher.getByShortName(teacherList, firstNameChar+"."+lastName); - return validateTeacher(teacher, String.valueOf(firstNameChar), lastName); - } - @NonNull - private Teacher validateTeacher(Teacher teacher, String firstName, String lastName) { - if (teacher == null) { - teacher = new Teacher(profileId, -1, firstName, lastName); - teacher.id = crc16(teacher.getShortName().getBytes()); - teacherList.add(teacher); - } - if (firstName.length() > 1) - teacher.name = firstName; - teacher.surname = lastName; - return teacher; - } - - private Teacher searchTeacherByLastFirst(String nameLastFirst) { - String[] nameParts = nameLastFirst.split(" ", Integer.MAX_VALUE); - if (nameParts.length == 1) - return searchTeacher(nameParts[0], ""); - return searchTeacher(nameParts[1], nameParts[0]); - } - private Teacher searchTeacherByFirstLast(String nameFirstLast) { - String[] nameParts = nameFirstLast.split(" ", Integer.MAX_VALUE); - if (nameParts.length == 1) - return searchTeacher(nameParts[0], ""); - return searchTeacher(nameParts[0], nameParts[1]); - } - private Teacher searchTeacherByFDotLast(String nameFDotLast) { - String[] nameParts = nameFDotLast.split("\\.", Integer.MAX_VALUE); - if (nameParts.length == 1) - return searchTeacher(nameParts[0], ""); - return searchTeacher(nameParts[0].charAt(0), nameParts[1]); - } - private Teacher searchTeacherByFDotSpaceLast(String nameFDotSpaceLast) { - String[] nameParts = nameFDotSpaceLast.split("\\. ", Integer.MAX_VALUE); - if (nameParts.length == 1) - return searchTeacher(nameParts[0], ""); - return searchTeacher(nameParts[0].charAt(0), nameParts[1]); - } - - /* _____ _ _____ _ - | __ \ | | | __ \ | | - | | | | __ _| |_ __ _ | |__) |___ __ _ _ _ ___ ___| |_ ___ - | | | |/ _` | __/ _` | | _ // _ \/ _` | | | |/ _ \/ __| __/ __| - | |__| | (_| | || (_| | | | \ \ __/ (_| | |_| | __/\__ \ |_\__ \ - |_____/ \__,_|\__\__,_| |_| \_\___|\__, |\__,_|\___||___/\__|___/ - | | - |*/ - private void getTimetable() { - callback.onActionStarted(R.string.sync_action_syncing_timetable); - Date weekStart = Week.getWeekStart(); - if (Date.getToday().getWeekDay() > 4) { - weekStart.stepForward(0, 0, 7); - } - apiRequest(Request.builder() - .url(IDZIENNIK_URL +"/mod_panelRodzica/plan/WS_Plan.asmx/pobierzPlanZajec") - .userAgent(userAgent) - .addParameter("idPozDziennika", loginRegisterId) - .addParameter("pidRokSzkolny", loginSchoolYearId) - .addParameter("data", weekStart.getStringY_m_d()+"T10:00:00.000Z") - .postJson(), (result, response) -> { - JsonObject data = result.getAsJsonObject("d"); - if (data == null) { - finishWithError(new AppError(TAG, 697, CODE_MAINTENANCE, response, result)); - return; - } - List> lessonHours = new ArrayList<>(); - for (JsonElement jLessonHourEl : data.getAsJsonArray("GodzinyLekcyjne")) { - JsonObject jLessonHour = jLessonHourEl.getAsJsonObject(); - // jLessonHour - lessonHours.add(new Pair<>(Time.fromH_m(jLessonHour.get("Poczatek").getAsString()), Time.fromH_m(jLessonHour.get("Koniec").getAsString()))); - } - - for (JsonElement jLessonEl : data.getAsJsonArray("Przedmioty")) { - JsonObject jLesson = jLessonEl.getAsJsonObject(); - // jLesson - Subject rSubject = searchSubject(jLesson.get("Nazwa").getAsString(), jLesson.get("Id").getAsInt(), jLesson.get("Skrot").getAsString()); - Teacher rTeacher = searchTeacherByFDotLast(jLesson.get("Nauczyciel").getAsString()); - - int weekDay = jLesson.get("DzienTygodnia").getAsInt() - 1; - Pair lessonHour = lessonHours.get(jLesson.get("Godzina").getAsInt()); - if (lessonHour == null || lessonHour.first == null || lessonHour.second == null) - continue; - Lesson lessonObject = new Lesson( - profileId, - weekDay, - lessonHour.first, - lessonHour.second - ); - lessonObject.subjectId = rSubject.id; - lessonObject.teacherId = rTeacher.id; - lessonObject.teamId = teamClassId; - lessonObject.classroomName = jLesson.get("NazwaSali").getAsString(); - - lessonList.add(lessonObject); - - int type = jLesson.get("TypZastepstwa").getAsInt(); - if (type != -1) { - // we have a lesson change to process - LessonChange lessonChangeObject = new LessonChange( - profileId, - weekStart.clone().stepForward(0, 0, weekDay), - lessonObject.startTime, - lessonObject.endTime - ); - - lessonChangeObject.teamId = lessonObject.teamId; - lessonChangeObject.teacherId = lessonObject.teacherId; - lessonChangeObject.subjectId = lessonObject.subjectId; - lessonChangeObject.classroomName = lessonObject.classroomName; - switch (type) { - case 0: - lessonChangeObject.type = TYPE_CANCELLED; - break; - case 1: - case 2: - case 3: - case 4: - case 5: - lessonChangeObject.type = TYPE_CHANGE; - String newTeacher = jLesson.get("NauZastepujacy").getAsString(); - String newSubject = jLesson.get("PrzedmiotZastepujacy").getAsString(); - if (!newTeacher.equals("")) { - lessonChangeObject.teacherId = searchTeacherByFDotLast(newTeacher).id; - } - if (!newSubject.equals("")) { - lessonChangeObject.subjectId = searchSubject(newSubject, -1, "").id; - } - break; - } - - lessonChangeList.add(lessonChangeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_LESSON_CHANGE, lessonChangeObject.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - } - r("finish", "Timetable"); - }); - } - - private void getGrades() { - callback.onActionStarted(R.string.sync_action_syncing_grades); - apiRequest(Request.builder() - .url(IDZIENNIK_URL +"/mod_panelRodzica/oceny/WS_ocenyUcznia.asmx/pobierzOcenyUcznia") - .userAgent(userAgent) - .addParameter("idPozDziennika", loginRegisterId) - .postJson(), (result, response) -> { - JsonObject data = result.getAsJsonObject("d"); - if (data == null) { - finishWithError(new AppError(TAG, 782, CODE_MAINTENANCE, response, result)); - return; - } - JsonArray jSubjects = data.getAsJsonArray("Przedmioty"); - for (JsonElement jSubjectEl : jSubjects) { - JsonObject jSubject = jSubjectEl.getAsJsonObject(); - // jSubject - Subject rSubject = searchSubject(jSubject.get("Przedmiot").getAsString(), jSubject.get("IdPrzedmiotu").getAsInt(), jSubject.get("Przedmiot").getAsString()); - for (JsonElement jGradeEl : jSubject.getAsJsonArray("Oceny")) { - JsonObject jGrade = jGradeEl.getAsJsonObject(); - // jGrade - Teacher rTeacher = searchTeacherByLastFirst(jGrade.get("Wystawil").getAsString()); - - boolean countToTheAverage = jGrade.get("DoSredniej").getAsBoolean(); - float value = jGrade.get("WartoscDoSred").getAsFloat(); - - String gradeColor = jGrade.get("Kolor").getAsString(); - int colorInt = 0xff2196f3; - if (!gradeColor.isEmpty()) { - colorInt = Color.parseColor("#"+gradeColor); - } - - Grade gradeObject = new Grade( - profileId, - jGrade.get("idK").getAsLong(), - jGrade.get("Kategoria").getAsString(), - colorInt, - "", - jGrade.get("Ocena").getAsString(), - value, - value > 0 && countToTheAverage ? jGrade.get("Waga").getAsFloat() : 0, - jGrade.get("Semestr").getAsInt(), - rTeacher.id, - rSubject.id); - - switch (jGrade.get("Typ").getAsInt()) { - case 0: - JsonElement historyEl = jGrade.get("Historia"); - JsonArray history; - if (historyEl instanceof JsonArray && (history = historyEl.getAsJsonArray()).size() > 0) { - float sum = gradeObject.value * gradeObject.weight; - float count = gradeObject.weight; - for (JsonElement historyItemEl: history) { - JsonObject historyItem = historyItemEl.getAsJsonObject(); - - countToTheAverage = historyItem.get("DoSredniej").getAsBoolean(); - value = historyItem.get("WartoscDoSred").getAsFloat(); - float weight = historyItem.get("Waga").getAsFloat(); - - if (value > 0 && countToTheAverage) { - sum += value * weight; - count += weight; - } - - Grade historyObject = new Grade( - profileId, - gradeObject.id * -1, - historyItem.get("Kategoria").getAsString(), - Color.parseColor("#"+historyItem.get("Kolor").getAsString()), - historyItem.get("Uzasadnienie").getAsString(), - historyItem.get("Ocena").getAsString(), - value, - value > 0 && countToTheAverage ? weight * -1 : 0, - historyItem.get("Semestr").getAsInt(), - rTeacher.id, - rSubject.id); - historyObject.parentId = gradeObject.id; - - gradeList.add(historyObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_GRADE, historyObject.id, true, true, Date.fromY_m_d(historyItem.get("Data_wystaw").getAsString()).getInMillis())); - } - // update the current grade's value with an average of all historical grades and itself - if (sum > 0 && count > 0) { - gradeObject.value = sum / count; - } - gradeObject.isImprovement = true; // gradeObject is the improved grade. Originals are historyObjects - } - break; - case 1: - gradeObject.type = TYPE_SEMESTER1_FINAL; - gradeObject.name = Integer.toString((int) gradeObject.value); - gradeObject.weight = 0; - break; - case 2: - gradeObject.type = TYPE_YEAR_FINAL; - gradeObject.name = Integer.toString((int) gradeObject.value); - gradeObject.weight = 0; - break; - } - - gradeList.add(gradeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_GRADE, gradeObject.id, profile.getEmpty(), profile.getEmpty(), Date.fromY_m_d(jGrade.get("Data_wystaw").getAsString()).getInMillis())); - } - } - r("finish", "Grades"); - }); - } - - private void getPropositionGrades() { - callback.onActionStarted(R.string.sync_action_syncing_proposition_grades); - apiRequest(Request.builder() - .url(IDZIENNIK_URL +"/mod_panelRodzica/brak_ocen/WS_BrakOcenUcznia.asmx/pobierzBrakujaceOcenyUcznia") - .userAgent(userAgent) - .addParameter("idPozDziennika", loginRegisterId) - .postJson(), (result, response) -> { - JsonObject data = result.getAsJsonObject("d"); - if (data == null) { - finishWithError(new AppError(TAG, 836, CODE_MAINTENANCE, response, result)); - return; - } - JsonArray jSubjects = data.getAsJsonArray("Przedmioty"); - for (JsonElement jSubjectEl : jSubjects) { - JsonObject jSubject = jSubjectEl.getAsJsonObject(); - // jSubject - Subject rSubject = searchSubject(jSubject.get("Przedmiot").getAsString(), -1, jSubject.get("Przedmiot").getAsString()); - String semester1Proposed = jSubject.get("OcenaSem1").getAsString(); - String semester2Proposed = jSubject.get("OcenaSem2").getAsString(); - int semester1Value = getWordGradeValue(semester1Proposed); - int semester2Value = getWordGradeValue(semester2Proposed); - long semester1Id = rSubject.id * -100 - 1; - long semester2Id = rSubject.id * -100 - 2; - - if (!semester1Proposed.equals("")) { - Grade gradeObject = new Grade( - profileId, - semester1Id, - "", - -1, - "", - Integer.toString(semester1Value), - semester1Value, - 0, - 1, - -1, - rSubject.id); - - gradeObject.type = TYPE_SEMESTER1_PROPOSED; - - gradeList.add(gradeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_GRADE, gradeObject.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - - if (!semester2Proposed.equals("")) { - Grade gradeObject = new Grade( - profileId, - semester2Id, - "", - -1, - "", - Integer.toString(semester2Value), - semester2Value, - 0, - 2, - -1, - rSubject.id); - - gradeObject.type = TYPE_YEAR_PROPOSED; - - gradeList.add(gradeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_GRADE, gradeObject.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - } - r("finish", "PropositionGrades"); - }); - } - - private int examsYear = Date.getToday().year; - private int examsMonth = Date.getToday().month; - private int examsMonthsChecked = 0; - private boolean examsNextMonthChecked = false; // TO DO temporary // no more // idk - private void getExams() { - callback.onActionStarted(R.string.sync_action_syncing_exams); - JsonObject postData = new JsonObject(); - postData.addProperty("idP", loginRegisterId); - postData.addProperty("rok", examsYear); - postData.addProperty("miesiac", examsMonth); - JsonObject param = new JsonObject(); - param.addProperty("strona", 1); - param.addProperty("iloscNaStrone", "99"); - param.addProperty("iloscRekordow", -1); - param.addProperty("kolumnaSort", "ss.Nazwa,sp.Data_sprawdzianu"); - param.addProperty("kierunekSort", 0); - param.addProperty("maxIloscZaznaczonych", 0); - param.addProperty("panelFiltrow", 0); - postData.add("param", param); - - apiRequest(Request.builder() - .url(IDZIENNIK_URL +"/mod_panelRodzica/sprawdziany/mod_sprawdzianyPanel.asmx/pobierzListe") - .userAgent(userAgent) - .setJsonBody(postData), (result, response) -> { - JsonObject data = result.getAsJsonObject("d"); - if (data == null) { - finishWithError(new AppError(TAG, 921, CODE_MAINTENANCE, response, result)); - return; - } - for (JsonElement jExamEl : data.getAsJsonArray("ListK")) { - JsonObject jExam = jExamEl.getAsJsonObject(); - // jExam - long eventId = jExam.get("_recordId").getAsLong(); - Subject rSubject = searchSubject(jExam.get("przedmiot").getAsString(), -1, ""); - Teacher rTeacher = searchTeacherByLastFirst(jExam.get("wpisal").getAsString()); - Date examDate = Date.fromY_m_d(jExam.get("data").getAsString()); - Lesson lessonObject = Lesson.getByWeekDayAndSubject(lessonList, examDate.getWeekDay(), rSubject.id); - Time examTime = lessonObject == null ? null : lessonObject.startTime; - - int eventType = (jExam.get("rodzaj").getAsString().equals("sprawdzian/praca klasowa") ? Event.TYPE_EXAM : Event.TYPE_SHORT_QUIZ); - Event eventObject = new Event( - profileId, - eventId, - examDate, - examTime, - jExam.get("zakres").getAsString(), - -1, - eventType, - false, - rTeacher.id, - rSubject.id, - teamClassId - ); - - eventList.add(eventObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_EVENT, eventObject.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - - if (profile.getEmpty() && examsMonthsChecked < 3 /* how many months backwards to check? */) { - examsMonthsChecked++; - examsMonth--; - if (examsMonth < 1) { - examsMonth = 12; - examsYear--; - } - r("get", "Exams"); - } else if (!examsNextMonthChecked /* get also one month forward */) { - Date showDate = Date.getToday().stepForward(0, 1, 0); - examsYear = showDate.year; - examsMonth = showDate.month; - examsNextMonthChecked = true; - r("get", "Exams"); - } else { - r("finish", "Exams"); - } - }); - } - - private void getNotices() { - callback.onActionStarted(R.string.sync_action_syncing_notices); - apiRequest(Request.builder() - .url(IDZIENNIK_URL +"/mod_panelRodzica/uwagi/WS_uwagiUcznia.asmx/pobierzUwagiUcznia") - .userAgent(userAgent) - .addParameter("idPozDziennika", loginRegisterId) - .postJson(), (result, response) -> { - JsonObject data = result.getAsJsonObject("d"); - if (data == null) { - finishWithError(new AppError(TAG, 982, CODE_MAINTENANCE, response, result)); - return; - } - for (JsonElement jNoticeEl : data.getAsJsonArray("SUwaga")) { - JsonObject jNotice = jNoticeEl.getAsJsonObject(); - // jExam - long noticeId = crc16(jNotice.get("id").getAsString().getBytes()); - - Teacher rTeacher = searchTeacherByLastFirst(jNotice.get("Nauczyciel").getAsString()); - Date addedDate = Date.fromY_m_d(jNotice.get("Data").getAsString()); - - int nType = TYPE_NEUTRAL; - String jType = jNotice.get("Typ").getAsString(); - if (jType.equals("n")) { - nType = TYPE_NEGATIVE; - } else if (jType.equals("p")) { - nType = TYPE_POSITIVE; - } - - Notice noticeObject = new Notice( - profileId, - noticeId, - jNotice.get("Tresc").getAsString(), - jNotice.get("Semestr").getAsInt(), - nType, - rTeacher.id); - noticeList.add(noticeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_NOTICE, noticeObject.id, profile.getEmpty(), profile.getEmpty(), addedDate.getInMillis())); - } - r("finish", "Notices"); - }); - } - - private void getAnnouncements() { - callback.onActionStarted(R.string.sync_action_syncing_announcements); - if (loginStudentId == null) { - r("finish", "Announcements"); - return; - } - JsonObject postData = new JsonObject(); - postData.addProperty("uczenId", loginStudentId); - JsonObject param = new JsonObject(); - param.add("parametryFiltrow", new JsonArray()); - postData.add("param", param); - - apiRequest(Request.builder() - .url(IDZIENNIK_URL +"/mod_panelRodzica/tabOgl/WS_tablicaOgloszen.asmx/GetOgloszenia") - .userAgent(userAgent) - .setJsonBody(postData), (result, response) -> { - JsonObject data = result.getAsJsonObject("d"); - if (data == null) { - finishWithError(new AppError(TAG, 1033, CODE_MAINTENANCE, response, result)); - return; - } - for (JsonElement jAnnouncementEl : data.getAsJsonArray("ListK")) { - JsonObject jAnnouncement = jAnnouncementEl.getAsJsonObject(); - // jAnnouncement - long announcementId = jAnnouncement.get("Id").getAsLong(); - - Teacher rTeacher = searchTeacherByFirstLast(jAnnouncement.get("Autor").getAsString()); - long addedDate = Long.parseLong(jAnnouncement.get("DataDodania").getAsString().replaceAll("[^\\d]", "")); - Date startDate = Date.fromMillis(Long.parseLong(jAnnouncement.get("DataWydarzenia").getAsString().replaceAll("[^\\d]", ""))); - - Announcement announcementObject = new Announcement( - profileId, - announcementId, - jAnnouncement.get("Temat").getAsString(), - jAnnouncement.get("Tresc").getAsString(), - startDate, - null, - rTeacher.id - ); - announcementList.add(announcementObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_ANNOUNCEMENT, announcementObject.id, profile.getEmpty(), profile.getEmpty(), addedDate)); - } - r("finish", "Announcements"); - }); - } - - private int attendanceYear; - private int attendanceMonth; - private boolean attendancePrevMonthChecked = false; - private void getAttendance() { - callback.onActionStarted(R.string.sync_action_syncing_attendance); - apiRequest(Request.builder() - .url(IDZIENNIK_URL +"/mod_panelRodzica/obecnosci/WS_obecnosciUcznia.asmx/pobierzObecnosciUcznia") - .userAgent(userAgent) - .addParameter("idPozDziennika", loginRegisterId) - .addParameter("mc", attendanceMonth) - .addParameter("rok", attendanceYear) - .addParameter("dataTygodnia", "") - .postJson(), (result, response) -> { - JsonObject data = result.getAsJsonObject("d"); - if (data == null) { - finishWithError(new AppError(TAG, 1076, CODE_MAINTENANCE, response, result)); - return; - } - for (JsonElement jAttendanceEl : data.getAsJsonArray("Obecnosci")) { - JsonObject jAttendance = jAttendanceEl.getAsJsonObject(); - // jAttendance - int attendanceTypeIdziennik = jAttendance.get("TypObecnosci").getAsInt(); - if (attendanceTypeIdziennik == 5 || attendanceTypeIdziennik == 7) - continue; - Date attendanceDate = Date.fromY_m_d(jAttendance.get("Data").getAsString()); - Time attendanceTime = Time.fromH_m(jAttendance.get("OdDoGodziny").getAsString()); - if (attendanceDate.combineWith(attendanceTime) > System.currentTimeMillis()) - continue; - - long attendanceId = crc16(jAttendance.get("IdLesson").getAsString().getBytes()); - Subject rSubject = searchSubject(jAttendance.get("Przedmiot").getAsString(), jAttendance.get("IdPrzedmiot").getAsLong(), ""); - Teacher rTeacher = searchTeacherByFDotSpaceLast(jAttendance.get("PrzedmiotNauczyciel").getAsString()); - - String attendanceName = "obecność"; - int attendanceType = Attendance.TYPE_CUSTOM; - - switch (attendanceTypeIdziennik) { - case 1: /* nieobecność usprawiedliwiona */ - attendanceName = "nieobecność usprawiedliwiona"; - attendanceType = TYPE_ABSENT_EXCUSED; - break; - case 2: /* spóźnienie */ - attendanceName = "spóźnienie"; - attendanceType = TYPE_BELATED; - break; - case 3: /* nieobecność nieusprawiedliwiona */ - attendanceName = "nieobecność nieusprawiedliwiona"; - attendanceType = TYPE_ABSENT; - break; - case 4: /* zwolnienie */ - case 9: /* zwolniony / obecny */ - attendanceType = TYPE_RELEASED; - if (attendanceTypeIdziennik == 4) - attendanceName = "zwolnienie"; - if (attendanceTypeIdziennik == 9) - attendanceName = "zwolnienie / obecność"; - break; - case 0: /* obecny */ - case 8: /* Wycieczka */ - attendanceType = TYPE_PRESENT; - if (attendanceTypeIdziennik == 8) - attendanceName = "wycieczka"; - break; - } - - int semester = profile.dateToSemester(attendanceDate); - - Attendance attendanceObject = new Attendance( - profileId, - attendanceId, - rTeacher.id, - rSubject.id, - semester, - attendanceName, - attendanceDate, - attendanceTime, - attendanceType - ); - - attendanceList.add(attendanceObject); - if (attendanceObject.type != TYPE_PRESENT) { - metadataList.add(new Metadata(profileId, Metadata.TYPE_ATTENDANCE, attendanceObject.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - } - - int attendanceDateValue = attendanceYear *10000 + attendanceMonth *100; - if (profile.getEmpty() && attendanceDateValue > profile.getSemesterStart(1).getValue()) { - attendancePrevMonthChecked = true; // do not need to check prev month later - attendanceMonth--; - if (attendanceMonth < 1) { - attendanceMonth = 12; - attendanceYear--; - } - r("get", "Attendance"); - } else if (!attendancePrevMonthChecked /* get also the previous month */) { - attendanceMonth--; - if (attendanceMonth < 1) { - attendanceMonth = 12; - attendanceYear--; - } - attendancePrevMonthChecked = true; - r("get", "Attendance"); - } else { - r("finish", "Attendance"); - } - }); - } - - private void getLuckyNumberAndSemesterDates() { - if (profile.getLuckyNumberDate() != null && profile.getLuckyNumber() != -1 && profile.getLuckyNumberDate().getValue() == Date.getToday().getValue()) { - r("finish", "LuckyNumberAndSemesterDates"); - return; - } - if (loginBearerToken == null || loginStudentId == null) { - r("finish", "LuckyNumberAndSemesterDates"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_lucky_number); - apiRequest(Request.builder() - .url(IDZIENNIK_URL +"/api/Uczniowie/"+loginStudentId+"/AktualnyDziennik") - .header("Authorization", "Bearer "+loginBearerToken) - .userAgent(userAgent), (data, response) -> { - JsonObject settings = data.getAsJsonObject("ustawienia"); - if (settings == null) { - finishWithError(new AppError(TAG, 1188, CODE_MAINTENANCE, response, data)); - return; - } - // data, settings - profile.setLuckyNumber(-1); - profile.setLuckyNumberDate(today); - JsonElement luckyNumberEl = data.get("szczesliwyNumerek"); - if (luckyNumberEl != null && !(luckyNumberEl instanceof JsonNull)) { - profile.setLuckyNumber(luckyNumberEl.getAsInt()); - Time publishTime = Time.fromH_m(settings.get("godzinaPublikacjiSzczesliwegoLosu").getAsString()); - if (Time.getNow().getValue() > publishTime.getValue()) { - profile.getLuckyNumberDate().stepForward(0, 0, 1); // the lucky number is already for tomorrow - } - app.db.luckyNumberDao().add(new LuckyNumber(profileId, profile.getLuckyNumberDate(), profile.getLuckyNumber())); - } - - profile.setDateSemester1Start(Date.fromY_m_d(settings.get("poczatekSemestru1").getAsString())); - profile.setDateSemester2Start(Date.fromY_m_d(settings.get("koniecSemestru1").getAsString()).stepForward(0, 0, 1)); - profile.setDateYearEnd(Date.fromY_m_d(settings.get("koniecSemestru2").getAsString())); - - r("finish", "LuckyNumberAndSemesterDates"); - }); - } - - private void getMessagesInbox() { - if (loginBearerToken == null) { - r("finish", "MessagesInbox"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_messages); - apiRequestArray(Request.builder() - .url(IDZIENNIK_URL +"/api/Wiadomosci/Odebrane") - .header("Authorization", "Bearer "+loginBearerToken) - .userAgent(userAgent), (data, response) -> { - for (JsonElement jMessageEl: data) { - JsonObject jMessage = jMessageEl.getAsJsonObject(); - - String subject = jMessage.get("tytul").getAsString(); - if (subject.contains("(") && subject.startsWith("iDziennik - ")) - continue; - if (subject.startsWith("Uwaga dla ucznia (klasa:")) - continue; - - String messageIdStr = jMessage.get("id").getAsString(); - long messageId = crc32((messageIdStr+"0").getBytes()); - - String body = "[META:"+messageIdStr+";-1]"; - body += jMessage.get("tresc").getAsString().replaceAll("\n", "
"); - - long readDate = jMessage.get("odczytana").getAsBoolean() ? Date.fromIso(jMessage.get("wersjaRekordu").getAsString()) : 0; - long sentDate = Date.fromIso(jMessage.get("dataWyslania").getAsString()); - - JsonObject sender = jMessage.getAsJsonObject("nadawca"); - Teacher rTeacher = searchTeacher(sender.get("imie").getAsString(), sender.get("nazwisko").getAsString()); - rTeacher.loginId = sender.get("id").getAsString()+":"+sender.get("usr").getAsString(); - - Message message = new Message( - profileId, - messageId, - subject, - body, - jMessage.get("rekordUsuniety").getAsBoolean() ? TYPE_DELETED : TYPE_RECEIVED, - rTeacher.id, - -1 - ); - - MessageRecipient messageRecipient = new MessageRecipient( - profileId, - -1 /* me */, - -1, - readDate, - /*messageId*/ messageId - ); - - messageList.add(message); - messageRecipientList.add(messageRecipient); - messageMetadataList.add(new Metadata(profileId, Metadata.TYPE_MESSAGE, message.id, readDate > 0, readDate > 0 || profile.getEmpty(), sentDate)); - } - - r("finish", "MessagesInbox"); - }); - } - - private void getMessagesOutbox() { - if (loginBearerToken == null) { - r("finish", "MessagesOutbox"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_messages); - apiRequestArray(Request.builder() - .url(IDZIENNIK_URL +"/api/Wiadomosci/Wyslane") - .header("Authorization", "Bearer "+loginBearerToken) - .userAgent(userAgent), (data, response) -> { - for (JsonElement jMessageEl: data) { - JsonObject jMessage = jMessageEl.getAsJsonObject(); - - String messageIdStr = jMessage.get("id").getAsString(); - long messageId = crc32((messageIdStr+"1").getBytes()); - - String subject = jMessage.get("tytul").getAsString(); - - String body = "[META:"+messageIdStr+";-1]"; - body += jMessage.get("tresc").getAsString().replaceAll("\n", "
"); - - long sentDate = Date.fromIso(jMessage.get("dataWyslania").getAsString()); - - Message message = new Message( - profileId, - messageId, - subject, - body, - TYPE_SENT, - -1, - -1 - ); - - for (JsonElement recipientEl: jMessage.getAsJsonArray("odbiorcy")) { - JsonObject recipient = recipientEl.getAsJsonObject(); - String firstName = recipient.get("imie").getAsString(); - String lastName = recipient.get("nazwisko").getAsString(); - if (firstName.isEmpty() || lastName.isEmpty()) { - firstName = "usunięty"; - lastName = "użytkownik"; - } - Teacher rTeacher = searchTeacher(firstName, lastName); - rTeacher.loginId = recipient.get("id").getAsString()+":"+recipient.get("usr").getAsString(); - - MessageRecipient messageRecipient = new MessageRecipient( - profileId, - rTeacher.id, - -1, - -1, - /*messageId*/ messageId - ); - messageRecipientIgnoreList.add(messageRecipient); - } - - messageList.add(message); - metadataList.add(new Metadata(profileId, Metadata.TYPE_MESSAGE, message.id, true, true, sentDate)); - } - - r("finish", "MessagesOutbox"); - }); - } - - @Override - public Map getConfigurableEndpoints(Profile profile) { - return null; - } - - @Override - public boolean isEndpointEnabled(Profile profile, boolean defaultActive, String name) { - return defaultActive; - } - - @Override - public void syncMessages(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile) - { - } - - /* __ __ - | \/ | - | \ / | ___ ___ ___ __ _ __ _ ___ ___ - | |\/| |/ _ \/ __/ __|/ _` |/ _` |/ _ \/ __| - | | | | __/\__ \__ \ (_| | (_| | __/\__ \ - |_| |_|\___||___/___/\__,_|\__, |\___||___/ - __/ | - |__*/ - @Override - public void getMessage(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull MessageFull message, @NonNull MessageGetCallback messageCallback) { - if (message.body == null) - return; - String messageIdStr = null; - long messageIdBefore = -1; - Matcher matcher = Pattern.compile("\\[META:([A-z0-9]+);([0-9-]+)]").matcher(message.body); - if (matcher.find()) { - messageIdStr = matcher.group(1); - messageIdBefore = Long.parseLong(matcher.group(2)); - } - if (messageIdBefore != -1) { - boolean readByAll = true; - // load this message's recipient(s) data - message.recipients = app.db.messageRecipientDao().getAllByMessageId(profile.getId(), message.id); - for (MessageRecipientFull recipient: message.recipients) { - if (recipient.id == -1) - recipient.fullName = profile.getStudentNameLong(); - if (message.type == TYPE_SENT && recipient.readDate < 1) - readByAll = false; - } - if (!message.seen) { - app.db.metadataDao().setSeen(profile.getId(), message, true); - } - if (readByAll) { - // if a sent msg is not read by everyone, download it again to check the read status - new Handler(activityContext.getMainLooper()).post(() -> messageCallback.onSuccess(message)); - return; - } - } - - if (!prepare(activityContext, errorCallback, profile.getId(), profile, LoginStore.fromProfileFull(profile))) - return; - - String finalMessageIdStr = messageIdStr; - login(() -> apiRequest(Request.builder() - .url(IDZIENNIK_URL +"/mod_komunikator/WS_wiadomosci.asmx/PobierzWiadomosc") - .userAgent(userAgent) - .addParameter("idWiadomosci", finalMessageIdStr) - .addParameter("typWiadomosci", message.type == TYPE_SENT ? 1 : 0) - .postJson(), (result, response) -> { - JsonObject data = result.getAsJsonObject("d"); - if (data == null) { - finishWithError(new AppError(TAG, 1418, CODE_MAINTENANCE, response, result)); - return; - } - JsonObject jMessage = data.getAsJsonObject("Wiadomosc"); - if (jMessage == null) { - finishWithError(new AppError(TAG, 1423, CODE_MAINTENANCE, response, result)); - return; - } - - List messageRecipientList = new ArrayList<>(); - - long messageId = jMessage.get("_recordId").getAsLong(); - - message.body = message.body.replaceAll("\\[META:[A-z0-9]+;[0-9-]+]", "[META:"+ finalMessageIdStr +";"+ messageId +"]"); - - message.clearAttachments(); - for (JsonElement jAttachmentEl: jMessage.getAsJsonArray("ListaZal")) { - JsonObject jAttachment = jAttachmentEl.getAsJsonObject(); - message.addAttachment(jAttachment.get("Id").getAsLong(), jAttachment.get("Nazwa").getAsString(), -1); - } - - if (message.type == TYPE_RECEIVED) { - MessageRecipientFull recipient = new MessageRecipientFull(profileId, -1, message.id); - - String readDateStr = jMessage.get("DataOdczytania").getAsString(); - recipient.readDate = readDateStr.isEmpty() ? System.currentTimeMillis() : Date.fromIso(readDateStr); - - recipient.fullName = profile.getStudentNameLong(); - messageRecipientList.add(recipient); - } - else if (message.type == TYPE_SENT) { - teacherList = app.db.teacherDao().getAllNow(profileId); - for (JsonElement jReceiverEl: jMessage.getAsJsonArray("ListaOdbiorcow")) { - JsonObject jReceiver = jReceiverEl.getAsJsonObject(); - String receiverLastFirstName = jReceiver.get("NazwaOdbiorcy").getAsString(); - - Teacher teacher = searchTeacherByLastFirst(receiverLastFirstName); - - MessageRecipientFull recipient = new MessageRecipientFull(profileId, teacher.id, message.id); - - recipient.readDate = jReceiver.get("Status").getAsInt(); - - recipient.fullName = teacher.getFullName(); - messageRecipientList.add(recipient); - } - } - - if (!message.seen) { - app.db.metadataDao().setSeen(profileId, message, true); - } - app.db.messageDao().add(message); - app.db.messageRecipientDao().addAll((List)(List) messageRecipientList); // not addAllIgnore - - message.recipients = messageRecipientList; - - new Handler(activityContext.getMainLooper()).post(() -> messageCallback.onSuccess(message)); - })); - } - - @Override - public void getAttachment(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull MessageFull message, long attachmentId, @NonNull AttachmentGetCallback attachmentCallback) { - if (message.body == null) - return; - if (!prepare(activityContext, errorCallback, profile.getId(), profile, LoginStore.fromProfileFull(profile))) - return; - - login(() -> { - String fileName = message.attachmentNames.get(message.attachmentIds.indexOf(attachmentId)); - - long messageId = -1; - Matcher matcher = Pattern.compile("\\[META:([A-z0-9]+);([0-9-]+)]").matcher(message.body); - if (matcher.find()) { - messageId = Long.parseLong(matcher.group(2)); - } - - Request.Builder builder = Request.builder() - .url("https://iuczniowie.progman.pl/idziennik/mod_komunikator/Download.ashx") - .post() - .contentType(MediaTypeUtils.APPLICATION_FORM) - .addParameter("id", messageId) - .addParameter("fileName", fileName); - - new Handler(activityContext.getMainLooper()).post(() -> attachmentCallback.onSuccess(builder)); - }); - } - - @Override - public void getRecipientList(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull RecipientListGetCallback recipientListGetCallback) { - if (!prepare(activityContext, errorCallback, profile.getId(), profile, LoginStore.fromProfileFull(profile))) - return; - - if (System.currentTimeMillis() - profile.getLastReceiversSync() < 24 * 60 * 60 * 1000) { - AsyncTask.execute(() -> { - List teacherList = app.db.teacherDao().getAllNow(profileId); - new Handler(activityContext.getMainLooper()).post(() -> recipientListGetCallback.onSuccess(teacherList)); - }); - return; - } - - login(() -> { - List cookieList = app.cookieJar.loadForRequest(HttpUrl.get(IDZIENNIK_URL)); - for (Cookie cookie: cookieList) { - if (cookie.name().equalsIgnoreCase("Bearer")) { - loginBearerToken = cookie.value(); - } - } - loginStudentId = profile.getStudentData("studentId", null);// TODO: 2019-06-12 temporary duplicated token & ID extraction - - apiRequestArray(Request.builder() - .url(IDZIENNIK_URL + "/api/Wiadomosci/Odbiorcy?idUcznia="+loginStudentId) - .header("Authorization", "Bearer " + loginBearerToken) - .userAgent(userAgent), (result, response) -> { - teacherList = app.db.teacherDao().getAllNow(profileId); - for (JsonElement recipientEl: result) { - JsonObject recipient = recipientEl.getAsJsonObject(); - String name = recipient.get("nazwaKontaktu").getAsString(); - String loginId = recipient.get("idUzytkownika").getAsString(); - JsonArray typesArray = recipient.getAsJsonArray("typOsoby"); - List types = new ArrayList<>(); - for (JsonElement typeEl: typesArray) { - types.add(typeEl.getAsInt()); - } - String delimiter; - if (types.size() == 1 && types.get(0) >= 6) /* parent or student */ - delimiter = " ("; - else - delimiter = ": "; - String nameFirstLast = name.substring(0, name.indexOf(delimiter)); - Teacher teacher = searchTeacherByFirstLast(nameFirstLast); - teacher.loginId = loginId; - teacher.type = 0; - for (int type: types) { - switch (type) { - case 0: - teacher.setType(Teacher.TYPE_SCHOOL_ADMIN); - break; - case 1: - teacher.setType(Teacher.TYPE_SECRETARIAT); - break; - case 2: - teacher.setType(Teacher.TYPE_TEACHER); - teacher.typeDescription = name.substring(name.indexOf(": ")+2); - break; - case 3: - teacher.setType(Teacher.TYPE_PRINCIPAL); - break; - case 4: - teacher.setType(Teacher.TYPE_EDUCATOR); - break; - case 5: - teacher.setType(Teacher.TYPE_PEDAGOGUE); - break; - case 6: - teacher.setType(Teacher.TYPE_PARENT); - teacher.typeDescription = name.substring(name.indexOf(" (")+2, name.lastIndexOf(" (")); - break; - case 7: - teacher.setType(Teacher.TYPE_STUDENT); - break; - } - } - } - app.db.teacherDao().addAll(teacherList); - - profile.setLastReceiversSync(System.currentTimeMillis()); - app.db.profileDao().add(profile); - - new Handler(activityContext.getMainLooper()).post(() -> recipientListGetCallback.onSuccess(new ArrayList<>(teacherList))); - }); - }); - } - - @Override - public MessagesComposeInfo getComposeInfo(@NonNull ProfileFull profile) { - return new MessagesComposeInfo(0, 0, 180, 1983); - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Librus.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Librus.java deleted file mode 100644 index 5c7356e2..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Librus.java +++ /dev/null @@ -1,3872 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api; - -import android.content.Context; -import android.graphics.Color; -import android.os.AsyncTask; -import android.os.Handler; -import android.util.Base64; -import android.util.Pair; -import android.util.SparseArray; -import android.util.SparseIntArray; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.crashlytics.android.Crashlytics; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonNull; -import com.google.gson.JsonObject; -import com.google.gson.reflect.TypeToken; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.parser.Parser; -import org.jsoup.select.Elements; - -import java.text.DateFormat; -import java.text.DecimalFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import im.wangchao.mhttp.Request; -import im.wangchao.mhttp.Response; -import im.wangchao.mhttp.body.MediaTypeUtils; -import im.wangchao.mhttp.callback.JsonCallbackHandler; -import im.wangchao.mhttp.callback.TextCallbackHandler; -import pl.szczodrzynski.edziennik.App; -import pl.szczodrzynski.edziennik.BuildConfig; -import pl.szczodrzynski.edziennik.R; -import pl.szczodrzynski.edziennik.data.api.interfaces.AttachmentGetCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface; -import pl.szczodrzynski.edziennik.data.api.interfaces.LoginCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.MessageGetCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.RecipientListGetCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.SyncCallback; -import pl.szczodrzynski.edziennik.data.api.v2.models.DataStore; -import pl.szczodrzynski.edziennik.data.db.modules.announcements.Announcement; -import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance; -import pl.szczodrzynski.edziennik.data.db.modules.events.Event; -import pl.szczodrzynski.edziennik.data.db.modules.events.EventType; -import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade; -import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory; -import pl.szczodrzynski.edziennik.data.db.modules.lessons.Lesson; -import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange; -import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore; -import pl.szczodrzynski.edziennik.data.db.modules.luckynumber.LuckyNumber; -import pl.szczodrzynski.edziennik.data.db.modules.messages.Message; -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull; -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient; -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipientFull; -import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata; -import pl.szczodrzynski.edziennik.data.db.modules.notices.Notice; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull; -import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject; -import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher; -import pl.szczodrzynski.edziennik.data.db.modules.teachers.TeacherAbsence; -import pl.szczodrzynski.edziennik.data.db.modules.teams.Team; -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesComposeInfo; -import pl.szczodrzynski.edziennik.utils.models.Date; -import pl.szczodrzynski.edziennik.utils.models.Endpoint; -import pl.szczodrzynski.edziennik.utils.models.Time; -import pl.szczodrzynski.edziennik.utils.models.Week; -import pl.szczodrzynski.edziennik.utils.Utils; - -import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; -import static java.net.HttpURLConnection.HTTP_FORBIDDEN; -import static java.net.HttpURLConnection.HTTP_GONE; -import static java.net.HttpURLConnection.HTTP_NOT_FOUND; -import static java.net.HttpURLConnection.HTTP_OK; -import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_INVALID_LOGIN; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_LIBRUS_DISCONNECTED; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_LIBRUS_NOT_ACTIVATED; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_MAINTENANCE; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_OTHER; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_PROFILE_NOT_FOUND; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_SYNERGIA_NOT_ACTIVATED; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_ABSENT; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_ABSENT_EXCUSED; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_BELATED; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_PRESENT; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_RELEASED; -import static pl.szczodrzynski.edziennik.data.db.modules.events.Event.TYPE_PT_MEETING; -import static pl.szczodrzynski.edziennik.data.db.modules.events.Event.TYPE_TEACHER_ABSENCE; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_NORMAL; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER1_FINAL; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER1_PROPOSED; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER2_FINAL; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER2_PROPOSED; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_YEAR_FINAL; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_YEAR_PROPOSED; -import static pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange.TYPE_CANCELLED; -import static pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange.TYPE_CHANGE; -import static pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED; -import static pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT; -import static pl.szczodrzynski.edziennik.data.db.modules.notices.Notice.TYPE_NEGATIVE; -import static pl.szczodrzynski.edziennik.data.db.modules.notices.Notice.TYPE_NEUTRAL; -import static pl.szczodrzynski.edziennik.data.db.modules.notices.Notice.TYPE_POSITIVE; -import static pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher.TYPE_EDUCATOR; -import static pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher.TYPE_LIBRARIAN; -import static pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher.TYPE_OTHER; -import static pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher.TYPE_PARENTS_COUNCIL; -import static pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher.TYPE_PEDAGOGUE; -import static pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher.TYPE_SCHOOL_ADMIN; -import static pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher.TYPE_SCHOOL_PARENTS_COUNCIL; -import static pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher.TYPE_SECRETARIAT; -import static pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher.TYPE_SUPER_ADMIN; -import static pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher.TYPE_TEACHER; -import static pl.szczodrzynski.edziennik.utils.Utils.bs; -import static pl.szczodrzynski.edziennik.utils.Utils.c; -import static pl.szczodrzynski.edziennik.utils.Utils.contains; -import static pl.szczodrzynski.edziennik.utils.Utils.crc16; -import static pl.szczodrzynski.edziennik.utils.Utils.d; -import static pl.szczodrzynski.edziennik.utils.Utils.getGradeValue; -import static pl.szczodrzynski.edziennik.utils.Utils.strToInt; - -public class Librus implements EdziennikInterface { - public Librus(App app) { - this.app = app; - } - - private static final String TAG = "api.Librus"; - private static final String CLIENT_ID = "wmSyUMo8llDAs4y9tJVYY92oyZ6h4lAt7KCuy0Gv"; - private static final String REDIRECT_URL = "http://localhost/bar"; - private static final String AUTHORIZE_URL = "https://portal.librus.pl/oauth2/authorize?client_id="+CLIENT_ID+"&redirect_uri="+REDIRECT_URL+"&response_type=code"; - private static final String LOGIN_URL = "https://portal.librus.pl/rodzina/login/action"; - private static final String TOKEN_URL = "https://portal.librus.pl/oauth2/access_token"; - private static final String ACCOUNTS_URL = "https://portal.librus.pl/api/v2/SynergiaAccounts"; - private static final String ACCOUNT_URL = "https://portal.librus.pl/api/v2/SynergiaAccounts/fresh/"; // + login - private static final String API_URL = "https://api.librus.pl/2.0/"; - private static final String SYNERGIA_URL = "https://wiadomosci.librus.pl/module/"; - private static final String SYNERGIA_SANDBOX_URL = "https://sandbox.librus.pl/index.php?action="; - private static final String userAgent = "Dalvik/2.1.0 Android LibrusMobileApp"; - private static final String synergiaUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/62.0"; - - private App app; - private Context activityContext = null; - private SyncCallback callback = null; - private int profileId = -1; - private Profile profile = null; - private LoginStore loginStore = null; - private boolean fullSync = true; - private Date today; - private List targetEndpoints = new ArrayList<>(); - - // PROGRESS - private static final int PROGRESS_LOGIN = 10; - private int PROGRESS_COUNT = 1; - private int PROGRESS_STEP = (90/PROGRESS_COUNT); - - private int onlyFeature = FEATURE_ALL; - - private List teamList; - private List teacherList; - private List teacherAbsenceList; - private List subjectList; - private List lessonList; - private List lessonChangeList; - private List gradeCategoryList; - private List gradeList; - private List eventList; - private List eventTypeList; - private List noticeList; - private List attendanceList; - private List announcementList; - private List messageList; - private List messageRecipientList; - private List messageRecipientIgnoreList; - private List metadataList; - private List messageMetadataList; - - private static boolean fakeLogin = false; - private String librusEmail = null; - private String librusPassword = null; - private String synergiaLogin = null; - private String synergiaPassword = null; - private String synergiaLastLogin = null; - private long synergiaLastLoginTime = -1; - private boolean premium = false; - private boolean enableStandardGrades = true; - private boolean enablePointGrades = false; - private boolean enableDescriptiveGrades = false; - private boolean enableTextGrades = false; - private boolean enableBehaviourGrades = true; - private long unitId = -1; - private int startPointsSemester1 = 0; - private int startPointsSemester2 = 0; - - private boolean prepare(@NonNull Context activityContext, @NonNull SyncCallback callback, int profileId, @Nullable Profile profile, @NonNull LoginStore loginStore) { - this.activityContext = activityContext; - this.callback = callback; - this.profileId = profileId; - // here we must have a login store: either with a correct ID or -1 - // there may be no profile and that's when onLoginFirst happens - this.profile = profile; - this.loginStore = loginStore; - this.fullSync = profile == null || profile.getEmpty() || profile.shouldFullSync(activityContext); - this.today = Date.getToday(); - - DataStore ds = new DataStore(app.db, profileId); - - this.librusEmail = loginStore.getLoginData("email", ""); - this.librusPassword = loginStore.getLoginData("password", ""); - if (profile == null) { - this.synergiaLogin = null; - this.synergiaPassword = null; - } - else { - this.synergiaLogin = profile.getStudentData("accountLogin", null); - this.synergiaPassword = profile.getStudentData("accountPassword", null); - } - if (librusEmail.equals("") || librusPassword.equals("")) { - finishWithError(new AppError(TAG, 214, AppError.CODE_INVALID_LOGIN, "Login field is empty")); - return false; - } - this.premium = profile != null && profile.getStudentData("isPremium", false); - this.failed = 0; - fakeLogin = BuildConfig.DEBUG && librusEmail.toLowerCase().startsWith("fake"); - this.synergiaLastLogin = null; - this.synergiaLastLoginTime = -1; - - this.refreshTokenFailed = false; - - teamList = profileId == -1 ? new ArrayList<>() : app.db.teamDao().getAllNow(profileId); - teacherList = profileId == -1 ? new ArrayList<>() : app.db.teacherDao().getAllNow(profileId); - teacherAbsenceList = new ArrayList<>(); - subjectList = new ArrayList<>(); - lessonList = new ArrayList<>(); - lessonChangeList = new ArrayList<>(); - gradeCategoryList = new ArrayList<>(); - gradeList = new ArrayList<>(); - eventList = new ArrayList<>(); - eventTypeList = new ArrayList<>(); - noticeList = new ArrayList<>(); - attendanceList = new ArrayList<>(); - announcementList = new ArrayList<>(); - messageList = new ArrayList<>(); - messageRecipientList = new ArrayList<>(); - messageRecipientIgnoreList = new ArrayList<>(); - metadataList = new ArrayList<>(); - messageMetadataList = new ArrayList<>(); - - return true; - } - - @Override - public void sync(@NonNull Context activityContext, @NonNull SyncCallback callback, int profileId, @Nullable Profile profile, @NonNull LoginStore loginStore) { - if (!prepare(activityContext, callback, profileId, profile, loginStore)) - return; - - login(() -> { - targetEndpoints = new ArrayList<>(); - targetEndpoints.add("Me"); - targetEndpoints.add("Schools"); - targetEndpoints.add("Classes"); - targetEndpoints.add("VirtualClasses"); - targetEndpoints.add("Units"); - targetEndpoints.add("Users"); - targetEndpoints.add("Subjects"); - targetEndpoints.add("Classrooms"); - targetEndpoints.add("Substitutions"); - targetEndpoints.add("Timetables"); - targetEndpoints.add("Colors"); - - targetEndpoints.add("SavedGradeCategories"); - targetEndpoints.add("GradesCategories"); - targetEndpoints.add("PointGradesCategories"); - targetEndpoints.add("DescriptiveGradesCategories"); - //targetEndpoints.add("TextGradesCategories"); - targetEndpoints.add("BehaviourGradesCategories"); // TODO: 2019-04-30 - targetEndpoints.add("SaveGradeCategories"); - - targetEndpoints.add("Grades"); - targetEndpoints.add("PointGrades"); - targetEndpoints.add("DescriptiveGrades"); - targetEndpoints.add("TextGrades"); - targetEndpoints.add("BehaviourGrades"); - targetEndpoints.add("GradesComments"); - - targetEndpoints.add("Events"); - targetEndpoints.add("TeacherFreeDays"); - targetEndpoints.add("CustomTypes"); - targetEndpoints.add("Homework"); - targetEndpoints.add("LuckyNumbers"); - targetEndpoints.add("Notices"); - targetEndpoints.add("AttendanceTypes"); - targetEndpoints.add("Attendance"); - targetEndpoints.add("Announcements"); - targetEndpoints.add("PtMeetings"); - - /*if (isEndpointEnabled(profile, true, "SchoolFreeDays")) - targetEndpoints.add("SchoolFreeDays"); - if (isEndpointEnabled(profile, true, "ClassFreeDays")) - targetEndpoints.add("ClassFreeDays");*/ - targetEndpoints.add("MessagesLogin"); - targetEndpoints.add("MessagesInbox"); - targetEndpoints.add("MessagesOutbox"); - targetEndpoints.add("Finish"); - PROGRESS_COUNT = targetEndpoints.size()-1; - PROGRESS_STEP = (90/PROGRESS_COUNT); - begin(); - }); - } - @Override - public void syncFeature(@NonNull Context activityContext, @NonNull SyncCallback callback, @NonNull ProfileFull profile, int ... featureList) { - if (featureList == null) { - sync(activityContext, callback, profile.getId(), profile, LoginStore.fromProfileFull(profile)); - return; - } - if (!prepare(activityContext, callback, profile.getId(), profile, LoginStore.fromProfileFull(profile))) - return; - - login(() -> { - targetEndpoints = new ArrayList<>(); - if (featureList.length == 1) - onlyFeature = featureList[0]; - targetEndpoints.add("Me"); - targetEndpoints.add("Schools"); - targetEndpoints.add("Classes"); - targetEndpoints.add("VirtualClasses"); - targetEndpoints.add("Units"); - targetEndpoints.add("Users"); - targetEndpoints.add("Subjects"); - targetEndpoints.add("Colors"); - boolean hasMessagesLogin = false; - for (int feature: featureList) { - switch (feature) { - case FEATURE_TIMETABLE: - targetEndpoints.add("Classrooms"); - targetEndpoints.add("Substitutions"); - targetEndpoints.add("Timetables"); - break; - case FEATURE_AGENDA: - targetEndpoints.add("Events"); - targetEndpoints.add("CustomTypes"); - targetEndpoints.add("PtMeetings"); - targetEndpoints.add("SchoolFreeDays"); - targetEndpoints.add("TeacherFreeDays"); - break; - case FEATURE_GRADES: - targetEndpoints.add("SavedGradeCategories"); - targetEndpoints.add("GradesCategories"); - targetEndpoints.add("PointGradesCategories"); - targetEndpoints.add("DescriptiveGradesCategories"); - //targetEndpoints.add("TextGradesCategories"); - targetEndpoints.add("BehaviourGradesCategories"); // TODO: 2019-04-30 - targetEndpoints.add("SaveGradeCategories"); - - targetEndpoints.add("Grades"); - targetEndpoints.add("PointGrades"); - targetEndpoints.add("DescriptiveGrades"); - targetEndpoints.add("TextGrades"); - targetEndpoints.add("BehaviourGrades"); - - targetEndpoints.add("GradesComments"); - break; - case FEATURE_HOMEWORK: - targetEndpoints.add("Homework"); - break; - case FEATURE_NOTICES: - targetEndpoints.add("Notices"); - break; - case FEATURE_ATTENDANCE: - targetEndpoints.add("AttendanceTypes"); - targetEndpoints.add("Attendance"); - break; - case FEATURE_MESSAGES_INBOX: - if (!hasMessagesLogin) { - hasMessagesLogin = true; - targetEndpoints.add("MessagesLogin"); - } - targetEndpoints.add("MessagesInbox"); - break; - case FEATURE_MESSAGES_OUTBOX: - if (!hasMessagesLogin) { - hasMessagesLogin = true; - targetEndpoints.add("MessagesLogin"); - } - targetEndpoints.add("MessagesOutbox"); - break; - case FEATURE_ANNOUNCEMENTS: - targetEndpoints.add("Announcements"); - break; - } - } - targetEndpoints.add("LuckyNumbers"); - - /*if (isEndpointEnabled(profile, true, "SchoolFreeDays")) - targetEndpoints.add("SchoolFreeDays"); - if (isEndpointEnabled(profile, true, "ClassFreeDays")) - targetEndpoints.add("ClassFreeDays");*/ - targetEndpoints.add("Finish"); - PROGRESS_COUNT = targetEndpoints.size()-1; - PROGRESS_STEP = (90/PROGRESS_COUNT); - begin(); - }); - } - - private void begin() { - if (profile == null) { - finishWithError(new AppError(TAG, 214, AppError.CODE_PROFILE_NOT_FOUND, "Profile == null WTF???")); - return; - } - String accountToken = profile.getStudentData("accountToken", null); - d(TAG, "Beginning account "+ profile.getStudentNameLong() +" sync with token "+accountToken+". Full sync enabled "+fullSync); - synergiaAccessToken = accountToken; - - callback.onProgress(PROGRESS_LOGIN); - - r("get", null); - } - - private void r(String type, String endpoint) { - // endpoint == null when beginning - if (endpoint == null) - endpoint = targetEndpoints.get(0); - int index = -1; - for (String request: targetEndpoints) { - index++; - if (request.equals(endpoint)) { - break; - } - } - if (type.equals("finish")) { - // called when finishing the action - callback.onProgress(PROGRESS_STEP); - index++; - } - if (index > targetEndpoints.size()) { - finish(); - return; - } - d(TAG, "Called r("+type+", "+endpoint+"). Getting "+targetEndpoints.get(index)); - switch (targetEndpoints.get(index)) { - case "Me": - getMe(); - break; - case "Schools": - getSchools(); - break; - case "Classes": - getClasses(); - break; - case "VirtualClasses": - getVirtualClasses(); - break; - case "Units": - getUnits(); - break; - case "Users": - getUsers(); - break; - case "Subjects": - getSubjects(); - break; - case "Classrooms": - getClassrooms(); - break; - case "Timetables": - getTimetables(); - break; - case "Substitutions": - getSubstitutions(); - break; - case "Colors": - getColors(); - break; - case "SavedGradeCategories": - getSavedGradeCategories(); - break; - case "GradesCategories": - getGradesCategories(); - break; - case "PointGradesCategories": - getPointGradesCategories(); - break; - case "DescriptiveGradesCategories": - getDescriptiveGradesSkills(); - break; - case "TextGradesCategories": - getTextGradesCategories(); - break; - case "BehaviourGradesCategories": - getBehaviourGradesCategories(); - break; - case "SaveGradeCategories": - saveGradeCategories(); - break; - case "Grades": - getGrades(); - break; - case "PointGrades": - getPointGrades(); - break; - case "DescriptiveGrades": - getDescriptiveGrades(); - break; - case "TextGrades": - getTextGrades(); - break; - case "GradesComments": - getGradesComments(); - break; - case "BehaviourGrades": - getBehaviourGrades(); - break; - case "Events": - getEvents(); - break; - case "CustomTypes": - getCustomTypes(); - break; - case "Homework": - getHomework(); - break; - case "LuckyNumbers": - getLuckyNumbers(); - break; - case "Notices": - getNotices(); - break; - case "AttendanceTypes": - getAttendanceTypes(); - break; - case "Attendance": - getAttendance(); - break; - case "Announcements": - getAnnouncements(); - break; - case "PtMeetings": - getPtMeetings(); - break; - case "TeacherFreeDaysTypes": - getTeacherFreeDaysTypes(); - break; - case "TeacherFreeDays": - getTeacherFreeDays(); - break; - case "SchoolFreeDays": - getSchoolFreeDays(); - break; - case "MessagesLogin": - getMessagesLogin(); - break; - case "MessagesInbox": - getMessagesInbox(); - break; - case "MessagesOutbox": - getMessagesOutbox(); - break; - case "Finish": - finish(); - break; - } - } - private void saveData() { - if (teamList.size() > 0) { - //app.db.teamDao().clear(profileId); - app.db.teamDao().addAll(teamList); - } - if (teacherList.size() > 0 && teacherListChanged) - app.db.teacherDao().addAllIgnore(teacherList); - if (subjectList.size() > 0) - app.db.subjectDao().addAll(subjectList); - if (lessonList.size() > 0) { - app.db.lessonDao().clear(profileId); - app.db.lessonDao().addAll(lessonList); - } - if (lessonChangeList.size() > 0) - app.db.lessonChangeDao().addAll(lessonChangeList); - if (gradeCategoryList.size() > 0 && gradeCategoryListChanged) - app.db.gradeCategoryDao().addAll(gradeCategoryList); - if (gradeList.size() > 0) { - app.db.gradeDao().clear(profileId); - app.db.gradeDao().addAll(gradeList); - } - if (eventList.size() > 0) { - app.db.eventDao().removeFuture(profileId, Date.getToday()); - app.db.eventDao().addAll(eventList); - } - if (eventTypeList.size() > 0) - app.db.eventTypeDao().addAll(eventTypeList); - if (teacherAbsenceList.size() > 0) - app.db.teacherAbsenceDao().addAll(teacherAbsenceList); - if (noticeList.size() > 0) { - app.db.noticeDao().clear(profileId); - app.db.noticeDao().addAll(noticeList); - } - if (attendanceList.size() > 0) - app.db.attendanceDao().addAll(attendanceList); - if (announcementList.size() > 0) - app.db.announcementDao().addAll(announcementList); - if (messageList.size() > 0) - app.db.messageDao().addAllIgnore(messageList); - if (messageRecipientList.size() > 0) - app.db.messageRecipientDao().addAll(messageRecipientList); - if (messageRecipientIgnoreList.size() > 0) - app.db.messageRecipientDao().addAllIgnore(messageRecipientIgnoreList); - if (metadataList.size() > 0) - app.db.metadataDao().addAllIgnore(metadataList); - if (messageMetadataList.size() > 0) - app.db.metadataDao().setSeen(messageMetadataList); - } - private void finish() { - try { - saveData(); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 480, CODE_OTHER, app.getString(R.string.sync_error_saving_data), null, null, e, null)); - } - if (fullSync) { - profile.setLastFullSync(System.currentTimeMillis()); - fullSync = false; - } - profile.setEmpty(false); - callback.onSuccess(activityContext, new ProfileFull(profile, loginStore)); - } - private void finishWithError(AppError error) { - try { - saveData(); - } - catch (Exception e) { - Crashlytics.logException(e); - } - callback.onError(activityContext, error); - } - - /* _ _ - | | (_) - | | ___ __ _ _ _ __ - | | / _ \ / _` | | '_ \ - | |___| (_) | (_| | | | | | - |______\___/ \__, |_|_| |_| - __/ | - |__*/ - public void login(@NonNull LoginCallback loginCallback) - { - - authorizeCallback = new AuthorizeCallback() { - @Override - public void onCsrfToken(String csrfToken) { - d(TAG, "Found CSRF token: "+csrfToken); - login(csrfToken, librusEmail, librusPassword, librusLoginCallback); - } - - @Override - public void onAuthorizationCode(String code) { - d(TAG, "Found auth code: "+code); - accessToken(code, null, accessTokenCallback); - } - }; - - librusLoginCallback = redirectUrl -> { - fakeAuthorize = "authorize"; - authorize(AUTHORIZE_URL, authorizeCallback); - }; - - accessTokenCallback = new AccessTokenCallback() { - @Override - public void onSuccess(String tokenType, String accessToken, String refreshToken, int expiresIn) { - d(TAG, "Got tokens: "+tokenType+" "+accessToken); - d(TAG, "Got tokens: "+refreshToken); - loginStore.putLoginData("tokenType", tokenType); - loginStore.putLoginData("accessToken", accessToken); - loginStore.putLoginData("refreshToken", refreshToken); - loginStore.putLoginData("tokenExpiryTime", System.currentTimeMillis()/1000 + expiresIn); - getSynergiaToken(tokenType, accessToken, refreshToken, System.currentTimeMillis()/1000 + expiresIn); - } - - @Override - public void onError() { - d(TAG, "Beginning login (authorize)"); - authorize(AUTHORIZE_URL, authorizeCallback); - } - }; - - synergiaAccountsCallback = data -> { - d(TAG, "Accounts: "+data.toString()); - JsonArray accounts = data.getAsJsonArray("accounts"); - if (accounts.size() == 0) { - finishWithError(new AppError(TAG, 1237, CODE_OTHER, app.getString(R.string.sync_error_register_no_students), data)); - return; - } - long accountDataTime; - List accountIds = new ArrayList<>(); - List accountLogins = new ArrayList<>(); - List accountTokens = new ArrayList<>(); - List accountNamesLong = new ArrayList<>(); - List accountNamesShort = new ArrayList<>(); - accountIds.clear(); - accountLogins.clear(); - accountTokens.clear(); - accountNamesLong.clear(); - accountNamesShort.clear(); - accountDataTime = data.get("lastModification").getAsLong(); - for (JsonElement accountEl: accounts) { - JsonObject account = accountEl.getAsJsonObject(); - - JsonElement state = account.get("state"); - if (state != null && !(state instanceof JsonNull)) { - if (state.getAsString().equals("requiring_an_action")) { - finishWithError(new AppError(TAG, 694, CODE_LIBRUS_DISCONNECTED, data)); - return; - } - if (state.getAsString().equals("need-activation")) { - finishWithError(new AppError(TAG, 701, CODE_SYNERGIA_NOT_ACTIVATED, data)); - return; - } - } - - accountIds.add(account.get("id").getAsInt()); - accountLogins.add(account.get("login").getAsString()); - accountTokens.add(account.get("accessToken").getAsString()); - accountNamesLong.add(account.get("studentName").getAsString()); - String[] nameParts = account.get("studentName").getAsString().split(" "); - accountNamesShort.add(nameParts[0]+" "+nameParts[1].charAt(0)+"."); - } - - List profileList = new ArrayList<>(); - for (int index = 0; index < accountIds.size(); index++) { - Profile newProfile = new Profile(); - newProfile.setStudentNameLong(accountNamesLong.get(index)); - newProfile.setStudentNameShort(accountNamesShort.get(index)); - newProfile.setName(newProfile.getStudentNameLong()); - newProfile.setSubname(librusEmail); - newProfile.setEmpty(true); - newProfile.setLoggedIn(true); - newProfile.putStudentData("accountId", accountIds.get(index)); - newProfile.putStudentData("accountLogin", accountLogins.get(index)); - newProfile.putStudentData("accountToken", accountTokens.get(index)); - newProfile.putStudentData("accountTokenTime", accountDataTime); - profileList.add(newProfile); - } - - callback.onLoginFirst(profileList, loginStore); - }; - - synergiaAccountCallback = data -> { - if (data == null) { - // 410 Gone - app.cookieJar.clearForDomain("portal.librus.pl"); - authorize(AUTHORIZE_URL, authorizeCallback); - return; - } - if (profile == null) { - // this cannot be run on a fresh login - finishWithError(new AppError(TAG, 1290, CODE_PROFILE_NOT_FOUND, "Profile == null", data)); - return; - } - d(TAG, "Account: "+data.toString()); - // synergiaAccount is executed when a synergia token needs a refresh - JsonElement id = data.get("id"); - JsonElement login = data.get("login"); - JsonElement accessToken = data.get("accessToken"); - if (id == null || login == null || accessToken == null) { - finishWithError(new AppError(TAG, 1284, CODE_OTHER, data)); - return; - } - profile.putStudentData("accountId", id.getAsInt()); - profile.putStudentData("accountLogin", login.getAsString()); - profile.putStudentData("accountToken", accessToken.getAsString()); - profile.putStudentData("accountTokenTime", System.currentTimeMillis() / 1000); - profile.setStudentNameLong(data.get("studentName").getAsString()); - String[] nameParts = data.get("studentName").getAsString().split(" "); - profile.setStudentNameShort(nameParts[0] + " " + nameParts[1].charAt(0) + "."); - loginCallback.onSuccess(); - }; - - String tokenType = loginStore.getLoginData("tokenType", "Bearer"); - String accessToken = loginStore.getLoginData("accessToken", null); - String refreshToken = loginStore.getLoginData("refreshToken", null); - long tokenExpiryTime = loginStore.getLoginData("tokenExpiryTime", (long)0); - String accountToken; - - if (profile != null - && (accountToken = profile.getStudentData("accountToken", null)) != null - && !accountToken.equals("") - && (System.currentTimeMillis() / 1000) - profile.getStudentData("accountTokenTime", (long)0) < 3 * 60 * 60) { - c(TAG, "synergia token should be valid"); - loginCallback.onSuccess(); - } - else { - getSynergiaToken(tokenType, accessToken, refreshToken, tokenExpiryTime); - } - } - private String synergiaAccessToken = ""; - private void getSynergiaToken(String tokenType, String accessToken, String refreshToken, long tokenExpiryTime) { - c(TAG, "we have no synergia token or it expired"); - if (!tokenType.equals("") - && refreshToken != null - && accessToken != null - && tokenExpiryTime-30 > System.currentTimeMillis() / 1000) { - c(TAG, "we have a valid librus token, so we can use the API"); - // we have to decide whether we can already proceed getting the synergiaToken - // or a list of students - if (profile != null) { - app.cookieJar.clearForDomain("portal.librus.pl"); - c(TAG, "user is logged in, refreshing synergia token"); - d(TAG, "Librus token: "+accessToken); - synergiaAccount(tokenType, accessToken, profile.getStudentData("accountLogin", null), synergiaAccountCallback); - } - else { - // this *should* be executed only once. ever. - c(TAG, "user is not logged in, getting all the accounts"); - synergiaAccounts(tokenType, accessToken, synergiaAccountsCallback); - } - } else if (refreshToken != null) { - c(TAG, "we don't have a valid token or it expired"); - c(TAG, "but we have a refresh token"); - d(TAG, "Token expired at " + tokenExpiryTime + ", " + (System.currentTimeMillis() / 1000 - tokenExpiryTime) + " seconds ago"); - app.cookieJar.clearForDomain("portal.librus.pl"); - accessToken(null, refreshToken, accessTokenCallback); - } else { - c(TAG, "we don't have any of the needed librus tokens"); - c(TAG, "we need to log in and generate"); - app.cookieJar.clearForDomain("portal.librus.pl"); - authorize(AUTHORIZE_URL, authorizeCallback); - } - } - public boolean loginSynergia(@NonNull LoginCallback loginCallback) - { - if (profile == null) { - return false; - } - if (synergiaLogin == null || synergiaPassword == null || synergiaLogin.equals("") || synergiaPassword.equals("")) { - finishWithError(new AppError(TAG, 1152, CODE_INVALID_LOGIN, "Login field is empty")); - return false; - } - if (System.currentTimeMillis() - synergiaLastLoginTime < 10 * 60 * 1000 && synergiaLogin.equals(synergiaLastLogin)) {// 10 minutes - loginCallback.onSuccess(); - return true; - } - - String escapedPassword = synergiaPassword.replace("&", "&");// TODO: 2019-05-07 check other chars to escape - - String body = - "\n" + - "

\n" + - " \n" + - " "+synergiaLogin+"\n" + - " "+escapedPassword+"\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - ""; - synergiaRequest("Login", body, data -> { - if (data == null) { - finishWithError(new AppError(TAG, 1176, AppError.CODE_MAINTENANCE, "data == null (975)")); - return; - } - String error = data.select("response Login status").text(); - if (error.equals("ok")) { - synergiaLastLoginTime = System.currentTimeMillis(); - synergiaLastLogin = synergiaLogin; - loginCallback.onSuccess(); - } - else { - finishWithError(new AppError(TAG, 1186, CODE_INVALID_LOGIN, (Response) null, data.outerHtml())); - } - }); - return true; - } - - /* _ _ _ _ _ _ _ - | | | | | | ___ | | | | | | - | |__| | ___| |_ __ ___ _ __ ___ ( _ ) ___ __ _| | | |__ __ _ ___| | _____ - | __ |/ _ \ | '_ \ / _ \ '__/ __| / _ \/\ / __/ _` | | | '_ \ / _` |/ __| |/ / __| - | | | | __/ | |_) | __/ | \__ \ | (_> < | (_| (_| | | | |_) | (_| | (__| <\__ \ - |_| |_|\___|_| .__/ \___|_| |___/ \___/\/ \___\__,_|_|_|_.__/ \__,_|\___|_|\_\___/ - | | - |*/ - private String fakeAuthorize = "authorize"; - private void authorize(String url, AuthorizeCallback authorizeCallback) { - callback.onActionStarted(R.string.sync_action_authorizing); - Request.builder() - .url(fakeLogin ? "http://szkolny.eu/librus/"+fakeAuthorize+".php" : url) - .userAgent(userAgent) - .withClient(app.httpLazy) - .callback(new TextCallbackHandler() { - @Override - public void onSuccess(String data, Response response) { - //d("headers "+response.headers().toString()); - String location = response.headers().get("Location"); - if (location != null) { - Matcher authMatcher = Pattern.compile(REDIRECT_URL+"\\?code=([A-z0-9]+?)$", Pattern.DOTALL | Pattern.MULTILINE).matcher(location); - if (authMatcher.find()) { - authorizeCallback.onAuthorizationCode(authMatcher.group(1)); - } - else { - //callback.onError(activityContext, Edziennik.CODE_OTHER, "Auth code not found: "+location); - authorize(location, authorizeCallback); - } - } - else { - Matcher csrfMatcher = Pattern.compile("name=\"csrf-token\" content=\"([A-z0-9=+/\\-_]+?)\"", Pattern.DOTALL).matcher(data); - if (csrfMatcher.find()) { - authorizeCallback.onCsrfToken(csrfMatcher.group(1)); - } - else { - finishWithError(new AppError(TAG, 463, CODE_OTHER, "CSRF token not found.", response, data)); - } - } - } - - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 207, CODE_OTHER, response, throwable)); - } - }) - .build() - .enqueue(); - } - - private void login(String csrfToken, String email, String password, LibrusLoginCallback librusLoginCallback) { - callback.onActionStarted(R.string.sync_action_logging_in); - Request.builder() - .url(fakeLogin ? "http://szkolny.eu/librus/login_action.php" : LOGIN_URL) - .userAgent(userAgent) - .addParameter("email", email) - .addParameter("password", password) - .addHeader("X-CSRF-TOKEN", csrfToken) - .contentType(MediaTypeUtils.APPLICATION_JSON) - .post() - .callback(new JsonCallbackHandler() { - @Override - public void onSuccess(JsonObject data, Response response) { - if (data == null) { - if (response.parserErrorBody != null && response.parserErrorBody.contains("link aktywacyjny")) { - finishWithError(new AppError(TAG, 487, CODE_LIBRUS_NOT_ACTIVATED, response)); - return; - } - finishWithError(new AppError(TAG, 489, CODE_MAINTENANCE, response)); - return; - } - if (data.get("errors") != null) { - finishWithError(new AppError(TAG, 490, CODE_OTHER, data.get("errors").getAsJsonArray().get(0).getAsString(), response, data)); - return; - } - librusLoginCallback.onLogin(data.get("redirect") != null ? data.get("redirect").getAsString() : ""); - } - - @Override - public void onFailure(Response response, Throwable throwable) { - if (response.code() == 403 - || response.code() == 401) { - finishWithError(new AppError(TAG, 248, AppError.CODE_INVALID_LOGIN, response, throwable)); - return; - } - finishWithError(new AppError(TAG, 251, CODE_OTHER, response, throwable)); - } - }) - .build() - .enqueue(); - } - - private boolean refreshTokenFailed = false; - private void accessToken(String code, String refreshToken, AccessTokenCallback accessTokenCallback) { - callback.onActionStarted(R.string.sync_action_getting_token); - List> params = new ArrayList<>(); - params.add(new Pair<>("client_id", CLIENT_ID)); - if (code != null) { - params.add(new Pair<>("grant_type", "authorization_code")); - params.add(new Pair<>("code", code)); - params.add(new Pair<>("redirect_uri", REDIRECT_URL)); - } - else if (refreshToken != null) { - params.add(new Pair<>("grant_type", "refresh_token")); - params.add(new Pair<>("refresh_token", refreshToken)); - } - Request.builder() - .url(fakeLogin ? "http://szkolny.eu/librus/access_token.php" : TOKEN_URL) - .userAgent(userAgent) - .addParams(params) - .allowErrorCode(HTTP_UNAUTHORIZED) - .post() - .callback(new JsonCallbackHandler() { - @Override - public void onSuccess(JsonObject data, Response response) { - if (data == null) { - finishWithError(new AppError(TAG, 539, CODE_MAINTENANCE, response)); - return; - } - if (data.get("error") != null) { - JsonElement message = data.get("message"); - JsonElement hint = data.get("hint"); - if (!refreshTokenFailed && refreshToken != null && hint != null && (hint.getAsString().equals("Token has been revoked") || hint.getAsString().equals("Token has expired"))) { - c(TAG, "refreshing the token failed. Trying to log in again."); - refreshTokenFailed = true; - accessTokenCallback.onError(); - return; - } - String errorText = data.get("error").getAsString()+" "+(message == null ? "" : message.getAsString())+" "+(hint == null ? "" : hint.getAsString()); - finishWithError(new AppError(TAG, 552, CODE_OTHER, errorText, response, data)); - return; - } - try { - accessTokenCallback.onSuccess( - data.get("token_type").getAsString(), - data.get("access_token").getAsString(), - data.get("refresh_token").getAsString(), - data.get("expires_in").getAsInt()); - } - catch (NullPointerException e) { - finishWithError(new AppError(TAG, 311, CODE_OTHER, response, e, data)); - } - } - - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 317, CODE_OTHER, response, throwable)); - } - }) - .build() - .enqueue(); - } - - private void synergiaAccounts(String tokenType, String accessToken, SynergiaAccountsCallback synergiaAccountsCallback) { - callback.onActionStarted(R.string.sync_action_getting_accounts); - Request.builder() - .url(fakeLogin ? "http://szkolny.eu/librus/synergia_accounts.php" : ACCOUNTS_URL) - .userAgent(userAgent) - .addHeader("Authorization", tokenType+" "+accessToken) - .get() - .allowErrorCode(HTTP_FORBIDDEN) - .allowErrorCode(HTTP_UNAUTHORIZED) - .allowErrorCode(HTTP_BAD_REQUEST) - .callback(new JsonCallbackHandler() { - @Override - public void onSuccess(JsonObject data, Response response) { - if (data == null) { - finishWithError(new AppError(TAG, 590, CODE_MAINTENANCE, response)); - return; - } - if (data.get("error") != null) { - JsonElement message = data.get("message"); - JsonElement hint = data.get("hint"); - String errorText = data.get("error").getAsString()+" "+(message == null ? "" : message.getAsString())+" "+(hint == null ? "" : hint.getAsString()); - finishWithError(new AppError(TAG, 597, CODE_OTHER, errorText, response, data)); - return; - } - try { - synergiaAccountsCallback.onSuccess(data); - } - catch (NullPointerException e) { - e.printStackTrace(); - finishWithError(new AppError(TAG, 358, CODE_OTHER, response, e, data)); - } - } - - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 364, CODE_OTHER, response, throwable)); - } - }) - .build() - .enqueue(); - } - - private boolean error410 = false; - - private void synergiaAccount(String tokenType, String accessToken, String accountLogin, SynergiaAccountCallback synergiaAccountCallback) { - callback.onActionStarted(R.string.sync_action_getting_account); - d(TAG, "Requesting "+(fakeLogin ? "http://szkolny.eu/librus/synergia_accounts_fresh.php?login="+accountLogin : ACCOUNT_URL+accountLogin)); - if (accountLogin == null) { // just for safety - synergiaAccounts(tokenType, accessToken, synergiaAccountsCallback); - return; - } - Request.builder() - .url(fakeLogin ? "http://szkolny.eu/librus/synergia_accounts_fresh.php?login="+accountLogin : ACCOUNT_URL+accountLogin) - .userAgent(userAgent) - .addHeader("Authorization", tokenType+" "+accessToken) - .get() - .allowErrorCode(HTTP_NOT_FOUND) - .allowErrorCode(HTTP_FORBIDDEN) - .allowErrorCode(HTTP_UNAUTHORIZED) - .allowErrorCode(HTTP_BAD_REQUEST) - .allowErrorCode(HTTP_GONE) - .callback(new JsonCallbackHandler() { - @Override - public void onSuccess(JsonObject data, Response response) { - if (data == null) { - finishWithError(new AppError(TAG, 641, CODE_MAINTENANCE, response)); - return; - } - if (response.code() == 410 && !error410) { - JsonElement reason = data.get("reason"); - if (reason != null && !(reason instanceof JsonNull) && reason.getAsString().equals("requires_an_action")) { - finishWithError(new AppError(TAG, 1078, CODE_LIBRUS_DISCONNECTED, response, data)); - return; - } - error410 = true; - synergiaAccountCallback.onSuccess(null); - return; - } - if (data.get("message") != null) { - String message = data.get("message").getAsString(); - if (message.equals("Account not found")) { - finishWithError(new AppError(TAG, 651, CODE_OTHER, app.getString(R.string.sync_error_register_student_not_associated_format, profile.getStudentNameLong(), accountLogin), response, data)); - return; - } - finishWithError(new AppError(TAG, 654, CODE_OTHER, message+"\n\n"+accountLogin, response, data)); - return; - } - if (response.code() == HTTP_OK) { - try { - synergiaAccountCallback.onSuccess(data); - } catch (NullPointerException e) { - e.printStackTrace(); - finishWithError(new AppError(TAG, 662, CODE_OTHER, response, e, data)); - } - } - else { - finishWithError(new AppError(TAG, 425, CODE_OTHER, response, data)); - } - } - - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 432, CODE_OTHER, response, throwable)); - } - }) - .build() - .enqueue(); - } - - private int failed = 0; - - private void apiRequest(String endpoint, ApiRequestCallback apiRequestCallback) { - d(TAG, "Requesting "+API_URL+endpoint); - Request.builder() - .url(fakeLogin ? "http://szkolny.eu/librus/api/"+endpoint : API_URL+endpoint) - .userAgent(userAgent) - .addHeader("Authorization", "Bearer "+synergiaAccessToken) - .get() - .allowErrorCode(HTTP_FORBIDDEN) - .allowErrorCode(HTTP_UNAUTHORIZED) - .allowErrorCode(HTTP_BAD_REQUEST) - .callback(new JsonCallbackHandler() { - @Override - public void onSuccess(JsonObject data, Response response) { - if (data == null) { - if (response.parserErrorBody != null && response.parserErrorBody.equals("Nieprawidłowy węzeł.")) { - apiRequestCallback.onSuccess(null); - return; - } - finishWithError(new AppError(TAG, 453, CODE_MAINTENANCE, response)); - return; - } - if (data.get("Status") != null) { - JsonElement message = data.get("Message"); - JsonElement code = data.get("Code"); - d(TAG, "apiRequest Error "+data.get("Status").getAsString()+" "+(message == null ? "" : message.getAsString())+" "+(code == null ? "" : code.getAsString())+"\n\n"+response.request().url().toString()); - if (message != null && !(message instanceof JsonNull) && message.getAsString().equals("Student timetable is not public")) { - try { - apiRequestCallback.onSuccess(null); - } - catch (NullPointerException e) { - e.printStackTrace(); - d(TAG, "apiRequest exception "+e.getMessage()); - finishWithError(new AppError(TAG, 503, CODE_OTHER, response, e, data)); - } - return; - } - if (code != null - && !(code instanceof JsonNull) - && (code.getAsString().equals("LuckyNumberIsNotActive") - || code.getAsString().equals("NotesIsNotActive") - || code.getAsString().equals("AccessDeny")) - ) { - try { - apiRequestCallback.onSuccess(null); - } - catch (NullPointerException e) { - e.printStackTrace(); - d(TAG, "apiRequest exception "+e.getMessage()); - finishWithError(new AppError(TAG, 504, CODE_OTHER, response, e, data)); - } - return; - } - String errorText = data.get("Status").getAsString()+" "+(message == null ? "" : message.getAsString())+" "+(code == null ? "" : code.getAsString()); - if (code != null && !(code instanceof JsonNull) && code.getAsString().equals("TokenIsExpired")) { - failed++; - d(TAG, "Trying to refresh synergia token, api request failed "+failed+" times now"); - if (failed > 1) { - d(TAG, "Giving up, failed "+failed+" times"); - finishWithError(new AppError(TAG, 485, CODE_OTHER, errorText, response, data)); - return; - } - String tokenType = loginStore.getLoginData("tokenType", "Bearer"); - String accessToken = loginStore.getLoginData("accessToken", null); - String refreshToken = loginStore.getLoginData("refreshToken", null); - long tokenExpiryTime = loginStore.getLoginData("tokenExpiryTime", (long)0); - getSynergiaToken(tokenType, accessToken, refreshToken, tokenExpiryTime); - return; - } - finishWithError(new AppError(TAG, 497, CODE_OTHER, errorText, response, data)); - return; - } - try { - apiRequestCallback.onSuccess(data); - } - catch (NullPointerException e) { - e.printStackTrace(); - d(TAG, "apiRequest exception "+e.getMessage()); - finishWithError(new AppError(TAG, 505, CODE_OTHER, response, e, data)); - } - } - - @Override - public void onFailure(Response response, Throwable throwable) { - if (response.code() == 405) { - // method not allowed - finishWithError(new AppError(TAG, 511, CODE_OTHER, response, throwable)); - return; - } - if (response.code() == 500) { - // TODO: 2019-09-10 dirty hotfix - if ("Classrooms".equals(endpoint)) { - apiRequestCallback.onSuccess(null); - return; - } - finishWithError(new AppError(TAG, 516, CODE_MAINTENANCE, response, throwable)); - return; - } - finishWithError(new AppError(TAG, 520, CODE_OTHER, response, throwable)); - } - }) - .build() - .enqueue(); - } - - private void synergiaRequest(String endpoint, String body, SynergiaRequestCallback synergiaRequestCallback) { - d(TAG, "Requesting "+SYNERGIA_URL+endpoint); - Request.builder() - .url(SYNERGIA_URL+endpoint) - .userAgent(synergiaUserAgent) - .setTextBody(body, MediaTypeUtils.APPLICATION_XML) - .callback(new TextCallbackHandler() { - @Override - public void onSuccess(String data, Response response) { - if ((data.contains("error") || data.contains("")) && !data.contains("Niepoprawny")) { - finishWithError(new AppError(TAG, 541, AppError.CODE_MAINTENANCE, response, data)); - return; - } - synergiaRequestCallback.onSuccess(Jsoup.parse(data, "", Parser.xmlParser())); - } - - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 556, CODE_OTHER, response, throwable)); - } - }) - .build() - .enqueue(); - } - - // CALLBACKS & INTERFACES - private interface AuthorizeCallback { - void onCsrfToken(String csrfToken); - void onAuthorizationCode(String code); - } - private interface LibrusLoginCallback { - void onLogin(String redirectUrl); - } - private interface AccessTokenCallback { - void onSuccess(String tokenType, String accessToken, String refreshToken, int expiresIn); - void onError(); - } - private interface SynergiaAccountsCallback { - void onSuccess(JsonObject data); - } - private interface SynergiaAccountCallback { - void onSuccess(JsonObject data); - } - private interface ApiRequestCallback { - void onSuccess(JsonObject data); - } - private interface SynergiaRequestCallback { - void onSuccess(Document data); - } - - private AuthorizeCallback authorizeCallback; - private LibrusLoginCallback librusLoginCallback; - private AccessTokenCallback accessTokenCallback; - private SynergiaAccountsCallback synergiaAccountsCallback; - private SynergiaAccountCallback synergiaAccountCallback; - - /* _____ _ _____ _ - | __ \ | | | __ \ | | - | | | | __ _| |_ __ _ | |__) |___ __ _ _ _ ___ ___| |_ ___ - | | | |/ _` | __/ _` | | _ // _ \/ _` | | | |/ _ \/ __| __/ __| - | |__| | (_| | || (_| | | | \ \ __/ (_| | |_| | __/\__ \ |_\__ \ - |_____/ \__,_|\__\__,_| |_| \_\___|\__, |\__,_|\___||___/\__|___/ - | | - |*/ - private void getMe() { - if (!fullSync) { - r("finish", "Me"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_account_info); - apiRequest("Me", data -> { - JsonObject me = data.get("Me").getAsJsonObject(); - me = me.get("Account").getAsJsonObject(); - //d("Got School: "+school.toString()); - try { - boolean premium = me.get("IsPremium").getAsBoolean(); - boolean premiumDemo = me.get("IsPremiumDemo").getAsBoolean(); - this.premium = premium || premiumDemo; - profile.putStudentData("isPremium", premium || premiumDemo); - r("finish", "Me"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 1316, CODE_OTHER, e, data)); - } - }); - } - - private Map> lessonRanges = new HashMap<>(); - private String schoolName = ""; - private void getSchools() { - if (!fullSync) { - try { - lessonRanges = app.gson.fromJson(profile.getStudentData("lessonRanges", "{}"), new TypeToken>>() { - }.getType()); - if (lessonRanges != null && lessonRanges.size() > 0) { - r("finish", "Schools"); - return; - } - } - catch (Exception e) { - e.printStackTrace(); - } - } - lessonRanges = new HashMap<>(); - callback.onActionStarted(R.string.sync_action_syncing_school_info); - apiRequest("Schools", data -> { - //d("Got School: "+school.toString()); - try { - JsonObject school = data.get("School").getAsJsonObject(); - int schoolId = school.get("Id").getAsInt(); - String schoolNameLong = school.get("Name").getAsString(); - StringBuilder schoolNameShort = new StringBuilder(); - for (String schoolNamePart: schoolNameLong.split(" ")) { - if (schoolNamePart.isEmpty()) - continue; - schoolNameShort.append(Character.toLowerCase(schoolNamePart.charAt(0))); - } - String schoolTown = school.get("Town").getAsString(); - schoolName = schoolId+schoolNameShort.toString()+"_"+schoolTown.toLowerCase(); - profile.putStudentData("schoolName", schoolName); - - lessonRanges.clear(); - int index = 0; - for (JsonElement lessonRangeEl: school.get("LessonsRange").getAsJsonArray()) { - JsonObject lr = lessonRangeEl.getAsJsonObject(); - JsonElement from = lr.get("From"); - JsonElement to = lr.get("To"); - if (from != null && to != null && !(from instanceof JsonNull) && !(to instanceof JsonNull)) { - lessonRanges.put(index, new Pair<>(Time.fromH_m(from.getAsString()), Time.fromH_m(to.getAsString()))); - } - index++; - } - profile.putStudentData("lessonRanges", app.gson.toJson(lessonRanges)); - r("finish", "Schools"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 1364, CODE_OTHER, e, data)); - } - }); - } - - private long teamClassId = -1; - private void getClasses() { - if (!fullSync) { - r("finish", "Classes"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_class); - apiRequest("Classes", data -> { - //d("Got Class: "+myClass.toString()); - try { - JsonObject myClass = data.get("Class").getAsJsonObject(); - String teamName = myClass.get("Number").getAsString() - + myClass.get("Symbol").getAsString(); - teamClassId = myClass.get("Id").getAsLong(); - teamList.add(new Team( - profileId, - teamClassId, - teamName, - 1, - schoolName+":"+teamName, - myClass.get("ClassTutor").getAsJsonObject().get("Id").getAsLong())); - JsonElement semester1Begin = myClass.get("BeginSchoolYear"); - JsonElement semester2Begin = myClass.get("EndFirstSemester"); - JsonElement yearEnd = myClass.get("EndSchoolYear"); - if (semester1Begin != null - && semester2Begin != null - && yearEnd != null - && !(semester1Begin instanceof JsonNull) - && !(semester2Begin instanceof JsonNull) - && !(yearEnd instanceof JsonNull)) { - profile.setDateSemester1Start(Date.fromY_m_d(semester1Begin.getAsString())); - profile.setDateSemester2Start(Date.fromY_m_d(semester2Begin.getAsString())); - profile.setDateYearEnd(Date.fromY_m_d(yearEnd.getAsString())); - } - JsonElement unit = myClass.get("Unit"); - if (unit != null && !(unit instanceof JsonNull)) { - unitId = unit.getAsJsonObject().get("Id").getAsLong(); - profile.putStudentData("unitId", unitId); - } - r("finish", "Classes"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 1411, CODE_OTHER, e, data)); - } - }); - } - - private void getVirtualClasses() { - if (!fullSync) { - r("finish", "VirtualClasses"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_teams); - apiRequest("VirtualClasses", data -> { - if (data == null) { - r("finish", "VirtualClasses"); - return; - } - try { - JsonArray classes = data.get("VirtualClasses").getAsJsonArray(); - for (JsonElement myClassEl: classes) { - JsonObject myClass = myClassEl.getAsJsonObject(); - String teamName = myClass.get("Name").getAsString(); - long teamId = myClass.get("Id").getAsLong(); - long teacherId = -1; - JsonElement el; - if ((el = myClass.get("Teacher")) != null) { - teacherId = el.getAsJsonObject().get("Id").getAsLong(); - } - teamList.add(new Team( - profileId, - teamId, - teamName, - 2, - schoolName + ":" + teamName, - teacherId)); - } - r("finish", "VirtualClasses"); - } catch (Exception e) { - finishWithError(new AppError(TAG, 1449, CODE_OTHER, e, data)); - } - }); - } - - private void getUnits() { - if (!fullSync) { - enableStandardGrades = profile.getStudentData("enableStandardGrades", true); - enablePointGrades = profile.getStudentData("enablePointGrades", false); - enableDescriptiveGrades = profile.getStudentData("enableDescriptiveGrades", false); - enableTextGrades = profile.getStudentData("enableTextGrades", false); - enableBehaviourGrades = profile.getStudentData("enableBehaviourGrades", true); - startPointsSemester1 = profile.getStudentData("startPointsSemester1", 0); - startPointsSemester2 = profile.getStudentData("startPointsSemester2", 0); - r("finish", "Units"); - return; - } - d(TAG, "Grades settings: "+enableStandardGrades+", "+enablePointGrades+", "+enableDescriptiveGrades); - callback.onActionStarted(R.string.sync_action_syncing_school_info); - apiRequest("Units", data -> { - if (data == null) { - r("finish", "Units"); - return; - } - JsonArray units = data.getAsJsonArray("Units"); - try { - long unitId = profile.getStudentData("unitId", (long)-1); - enableStandardGrades = true; // once a week or two (during a full sync) force getting the standard grade list. If there aren't any, disable it again later. - enableBehaviourGrades = true; - enableTextGrades = true; // TODO: 2019-05-13 if "DescriptiveGradesEnabled" are also TextGrades - profile.putStudentData("enableStandardGrades", true); - profile.putStudentData("enableBehaviourGrades", true); - profile.putStudentData("enableTextGrades", true); - for (JsonElement unitEl: units) { - JsonObject unit = unitEl.getAsJsonObject(); - - if (unit.get("Id").getAsLong() == unitId) { - JsonObject gradesSettings = unit.getAsJsonObject("GradesSettings"); - enablePointGrades = gradesSettings.get("PointGradesEnabled").getAsBoolean(); - enableDescriptiveGrades = gradesSettings.get("DescriptiveGradesEnabled").getAsBoolean(); - - JsonObject behaviourGradesSettings = unit.getAsJsonObject("BehaviourGradesSettings"); - JsonObject startPoints = behaviourGradesSettings.getAsJsonObject("StartPoints"); - - startPointsSemester1 = startPoints.get("Semester1").getAsInt(); - JsonElement startPointsSemester2El; - if ((startPointsSemester2El = startPoints.get("Semester2")) != null) { - startPointsSemester2 = startPointsSemester2El.getAsInt(); - } - else { - startPointsSemester2 = startPointsSemester1; - } - - profile.putStudentData("enablePointGrades", enablePointGrades); - profile.putStudentData("enableDescriptiveGrades", enableDescriptiveGrades); - profile.putStudentData("startPointsSemester1", startPointsSemester1); - profile.putStudentData("startPointsSemester2", startPointsSemester2); - break; - } - } - d(TAG, "Grades settings: "+enableStandardGrades+", "+enablePointGrades+", "+enableDescriptiveGrades); - r("finish", "Units"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 1506, CODE_OTHER, e, data)); - } - }); - } - - private boolean teacherListChanged = false; - private void getUsers() { - if (!fullSync) { - r("finish", "Users"); - teacherList = app.db.teacherDao().getAllNow(profileId); - teacherListChanged = false; - return; - } - callback.onActionStarted(R.string.sync_action_syncing_users); - apiRequest("Users", data -> { - if (data == null) { - r("finish", "Users"); - return; - } - JsonArray users = data.get("Users").getAsJsonArray(); - //d("Got Users: "+users.toString()); - try { - teacherListChanged = true; - for (JsonElement userEl : users) { - JsonObject user = userEl.getAsJsonObject(); - JsonElement firstName = user.get("FirstName"); - JsonElement lastName = user.get("LastName"); - teacherList.add(new Teacher( - profileId, - user.get("Id").getAsLong(), - firstName instanceof JsonNull ? "" : firstName.getAsString(), - lastName instanceof JsonNull ? "" : lastName.getAsString() - )); - } - r("finish", "Users"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 1544, CODE_OTHER, e, data)); - } - }); - } - - private void getSubjects() { - if (!fullSync) { - r("finish", "Subjects"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_subjects); - apiRequest("Subjects", data -> { - if (data == null) { - r("finish", "Subjects"); - return; - } - JsonArray subjects = data.get("Subjects").getAsJsonArray(); - //d("Got Subjects: "+subjects.toString()); - try { - for (JsonElement subjectEl : subjects) { - JsonObject subject = subjectEl.getAsJsonObject(); - JsonElement longName = subject.get("Name"); - JsonElement shortName = subject.get("Short"); - subjectList.add(new Subject( - profileId, - subject.get("Id").getAsLong(), - longName instanceof JsonNull ? "" : longName.getAsString(), - shortName instanceof JsonNull ? "" : shortName.getAsString() - )); - } - subjectList.add(new Subject( - profileId, - 1, - "Zachowanie", - "zach" - )); - r("finish", "Subjects"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 1588, CODE_OTHER, e, data)); - } - }); - } - - private SparseArray classrooms = new SparseArray<>(); - private void getClassrooms() { - //if (!fullSync) - // r("finish", "Classrooms"); - callback.onActionStarted(R.string.sync_action_syncing_classrooms); - apiRequest("Classrooms", data -> { - if (data == null) { - r("finish", "Classrooms"); - return; - } - if (data.get("Classrooms") == null) { - r("finish", "Classrooms"); - return; - } - JsonArray jClassrooms = data.get("Classrooms").getAsJsonArray(); - //d("Got Classrooms: "+jClassrooms.toString()); - classrooms.clear(); - try { - for (JsonElement classroomEl : jClassrooms) { - JsonObject classroom = classroomEl.getAsJsonObject(); - classrooms.put(classroom.get("Id").getAsInt(), classroom.get("Name").getAsString()); - } - r("finish", "Classrooms"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 1617, CODE_OTHER, e, data)); - } - }); - } - - private void getTimetables() { - callback.onActionStarted(R.string.sync_action_syncing_timetable); - Date weekStart = Week.getWeekStart(); - if (Date.getToday().getWeekDay() > 4) { - weekStart.stepForward(0, 0, 7); - } - apiRequest("Timetables?weekStart="+weekStart.getStringY_m_d(), data -> { - if (data == null) { - r("finish", "Timetables"); - return; - } - JsonObject timetables = data.get("Timetable").getAsJsonObject(); - try { - for (Map.Entry dayEl: timetables.entrySet()) { - JsonArray day = dayEl.getValue().getAsJsonArray(); - Date lessonDate = Date.fromY_m_d(dayEl.getKey()); - for (JsonElement lessonGroupEl: day) { - if ((lessonGroupEl instanceof JsonArray && ((JsonArray) lessonGroupEl).size() == 0) || lessonGroupEl instanceof JsonNull || lessonGroupEl == null) { - continue; - } - JsonArray lessonGroup = lessonGroupEl.getAsJsonArray(); - for (JsonElement lessonEl: lessonGroup) { - if ((lessonEl instanceof JsonArray && ((JsonArray) lessonEl).size() == 0) || lessonEl instanceof JsonNull || lessonEl == null) { - continue; - } - JsonObject lesson = lessonEl.getAsJsonObject(); - - boolean substitution = false; - boolean cancelled = false; - JsonElement isSubstitutionClass; - if ((isSubstitutionClass = lesson.get("IsSubstitutionClass")) != null) { - substitution = isSubstitutionClass.getAsBoolean(); - } - JsonElement isCanceled; - if ((isCanceled = lesson.get("IsCanceled")) != null) { - cancelled = isCanceled.getAsBoolean(); - } - - if (substitution && cancelled) { - // the lesson is probably shifted. Skip this one - continue; - } - - Time startTime = null; - Time endTime = null; - try { - startTime = Time.fromH_m(lesson.get(substitution && !cancelled ? "OrgHourFrom" : "HourFrom").getAsString()); - endTime = Time.fromH_m(lesson.get(substitution && !cancelled ? "OrgHourTo" : "HourTo").getAsString()); - } - catch (Exception ignore) { - try { - JsonElement lessonNo; - if (!((lessonNo = lesson.get("LessonNo")) instanceof JsonNull)) { - Pair timePair = lessonRanges.get(strToInt(lessonNo.getAsString())); - if (timePair != null) { - startTime = timePair.first; - endTime = timePair.second; - } - } - } - catch (Exception ignore2) { } - } - - - Lesson lessonObject = new Lesson( - profileId, - lesson.get("DayNo").getAsInt() - 1, - startTime, - endTime - ); - - JsonElement subject; - if ((subject = lesson.get(substitution && !cancelled ? "OrgSubject" : "Subject")) != null) { - lessonObject.subjectId = subject.getAsJsonObject().get("Id").getAsLong(); - } - - JsonElement teacher; - if ((teacher = lesson.get(substitution && !cancelled ? "OrgTeacher" : "Teacher")) != null) { - lessonObject.teacherId = teacher.getAsJsonObject().get("Id").getAsLong(); - } - - JsonElement myClass; - if ((myClass = lesson.get("Class")) != null) { - lessonObject.teamId = myClass.getAsJsonObject().get("Id").getAsLong(); - } - if (myClass == null && (myClass = lesson.get("VirtualClass")) != null) { - lessonObject.teamId = myClass.getAsJsonObject().get("Id").getAsLong(); - } - - JsonElement classroom; - JsonElement substitutionClassroom; - if (substitution && !cancelled) { - classroom = lesson.get("OrgClassroom"); - substitutionClassroom = lesson.get("Classroom"); - - if (classroom != null) - lessonObject.classroomName = classrooms.get(classroom.getAsJsonObject().get("Id").getAsInt()); - - if (substitutionClassroom != null) { - for (LessonChange lessonChange : lessonChangeList) { - if(lessonChange.lessonDate.compareTo(lessonDate) == 0) { - lessonChange.classroomName - = classrooms.get(substitutionClassroom.getAsJsonObject().get("Id").getAsInt()); - break; - } - } - } - } else { - classroom = lesson.get("Classroom"); - if (classroom != null) - lessonObject.classroomName = classrooms.get(classroom.getAsJsonObject().get("Id").getAsInt()); - } - - lessonList.add(lessonObject); - } - } - } - r("finish", "Timetables"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 1704, CODE_OTHER, e, data)); - } - }); - } - - private void getSubstitutions() { - callback.onActionStarted(R.string.sync_action_syncing_timetable_changes); - apiRequest("Calendars/Substitutions", data -> { - if (data == null) { - r("finish", "Substitutions"); - return; - } - - JsonArray substitutions = data.get("Substitutions").getAsJsonArray(); - try { - List ignoreList = new ArrayList<>(); - for (JsonElement substitutionEl : substitutions) { - JsonObject substitution = substitutionEl.getAsJsonObject(); - - String str_date = substitution.get("OrgDate").getAsString(); - Date lessonDate = Date.fromY_m_d(str_date); - - Time startTime = Time.getNow(); - JsonElement lessonNo; - if (!((lessonNo = substitution.get("OrgLessonNo")) instanceof JsonNull)) { - Pair timePair = lessonRanges.get(lessonNo.getAsInt()); - if (timePair != null) - startTime = timePair.first; - } - - JsonElement isShifted; - JsonElement isCancelled; - if ((isShifted = substitution.get("IsShifted")) != null && isShifted.getAsBoolean()) { - // a lesson is shifted - // add a TYPE_CANCELLED for the source lesson and a TYPE_CHANGE for the destination lesson - - // source lesson: cancel - LessonChange lessonCancelled = new LessonChange(profileId, lessonDate, startTime, startTime.clone().stepForward(0, 45, 0)); - lessonCancelled.type = TYPE_CANCELLED; - lessonChangeList.add(lessonCancelled); - metadataList.add(new Metadata(profileId, Metadata.TYPE_LESSON_CHANGE, lessonCancelled.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - - // target lesson: change - startTime = Time.getNow(); - if (!((lessonNo = substitution.get("LessonNo")) instanceof JsonNull)) { - Pair timePair = lessonRanges.get(lessonNo.getAsInt()); - if (timePair != null) - startTime = timePair.first; - } - - LessonChange lessonChanged = new LessonChange(profileId, lessonDate, startTime, startTime.clone().stepForward(0, 45, 0)); - lessonChanged.type = TYPE_CHANGE; - JsonElement subject; - if ((subject = substitution.get("Subject")) != null) { - lessonChanged.subjectId = subject.getAsJsonObject().get("Id").getAsLong(); - } - JsonElement teacher; - if ((teacher = substitution.get("Teacher")) != null) { - lessonChanged.teacherId = teacher.getAsJsonObject().get("Id").getAsLong(); - } - JsonElement myClass; - if ((myClass = substitution.get("Class")) != null) { - lessonChanged.teamId = myClass.getAsJsonObject().get("Id").getAsLong(); - } - if (myClass == null && (myClass = substitution.get("VirtualClass")) != null) { - lessonChanged.teamId = myClass.getAsJsonObject().get("Id").getAsLong(); - } - lessonChangeList.add(lessonChanged); - metadataList.add(new Metadata(profileId, Metadata.TYPE_LESSON_CHANGE, lessonChanged.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - - // ignore the target lesson in further array elements - it's already changed - ignoreList.add(lessonChanged.lessonDate.combineWith(lessonChanged.startTime)); - } - else if ((isCancelled = substitution.get("IsCancelled")) != null && isCancelled.getAsBoolean()) { - LessonChange lessonChange = new LessonChange(profileId, lessonDate, startTime, startTime.clone().stepForward(0, 45, 0)); - // if it's actually a lesson shift - ignore the target lesson cancellation - if (ignoreList.size() > 0 && ignoreList.contains(lessonChange.lessonDate.combineWith(lessonChange.startTime))) - continue; - lessonChange.type = TYPE_CANCELLED; - lessonChangeList.add(lessonChange); - metadataList.add(new Metadata(profileId, Metadata.TYPE_LESSON_CHANGE, lessonChange.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - else { - LessonChange lessonChange = new LessonChange(profileId, lessonDate, startTime, startTime.clone().stepForward(0, 45, 0)); - - lessonChange.type = TYPE_CHANGE; - - JsonElement subject; - if ((subject = substitution.get("Subject")) != null) { - lessonChange.subjectId = subject.getAsJsonObject().get("Id").getAsLong(); - } - JsonElement teacher; - if ((teacher = substitution.get("Teacher")) != null) { - lessonChange.teacherId = teacher.getAsJsonObject().get("Id").getAsLong(); - } - - JsonElement myClass; - if ((myClass = substitution.get("Class")) != null) { - lessonChange.teamId = myClass.getAsJsonObject().get("Id").getAsLong(); - } - if (myClass == null && (myClass = substitution.get("VirtualClass")) != null) { - lessonChange.teamId = myClass.getAsJsonObject().get("Id").getAsLong(); - } - - lessonChangeList.add(lessonChange); - metadataList.add(new Metadata(profileId, Metadata.TYPE_LESSON_CHANGE, lessonChange.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - - } - r("finish", "Substitutions"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 1822, CODE_OTHER, e, data)); - } - }); - } - - private SparseIntArray colors = new SparseIntArray(); - private void getColors() { - colors.put( 1, 0xFFF0E68C); - colors.put( 2, 0xFF87CEFA); - colors.put( 3, 0xFFB0C4DE); - colors.put( 4, 0xFFF0F8FF); - colors.put( 5, 0xFFF0FFFF); - colors.put( 6, 0xFFF5F5DC); - colors.put( 7, 0xFFFFEBCD); - colors.put( 8, 0xFFFFF8DC); - colors.put( 9, 0xFFA9A9A9); - colors.put(10, 0xFFBDB76B); - colors.put(11, 0xFF8FBC8F); - colors.put(12, 0xFFDCDCDC); - colors.put(13, 0xFFDAA520); - colors.put(14, 0xFFE6E6FA); - colors.put(15, 0xFFFFA07A); - colors.put(16, 0xFF32CD32); - colors.put(17, 0xFF66CDAA); - colors.put(18, 0xFF66CDAA); - colors.put(19, 0xFFC0C0C0); - colors.put(20, 0xFFD2B48C); - colors.put(21, 0xFF3333FF); - colors.put(22, 0xFF7B68EE); - colors.put(23, 0xFFBA55D3); - colors.put(24, 0xFFFFB6C1); - colors.put(25, 0xFFFF1493); - colors.put(26, 0xFFDC143C); - colors.put(27, 0xFFFF0000); - colors.put(28, 0xFFFF8C00); - colors.put(29, 0xFFFFD700); - colors.put(30, 0xFFADFF2F); - colors.put(31, 0xFF7CFC00); - r("finish", "Colors"); - /* - apiRequest("Colors", data -> { - JsonArray jColors = data.get("Colors").getAsJsonArray(); - d("Got Colors: "+jColors.toString()); - colors.clear(); - try { - for (JsonElement colorEl : jColors) { - JsonObject color = colorEl.getAsJsonObject(); - colors.put(color.get("Id").getAsInt(), Color.parseColor("#"+color.get("RGB").getAsString())); - } - } - catch (Exception e) { - e.printStackTrace(); - } - getGrades(); - });*/ - } - - private boolean gradeCategoryListChanged = false; - private void getSavedGradeCategories() { - gradeCategoryList = app.db.gradeCategoryDao().getAllNow(profileId); - gradeCategoryListChanged = false; - r("finish", "SavedGradeCategories"); - } - - private void saveGradeCategories() { - r("finish", "SaveGradeCategories"); - } - - private void getGradesCategories() { - if (!fullSync && false) { - // cancel every not-full sync; no need to download categories again - // every full sync it'll be enabled to make sure there are no grades - by getUnits - r("finish", "GradesCategories"); - return; - } - // not a full sync. Will get all grade categories. Clear the current list. - //gradeCategoryList.clear(); - - callback.onActionStarted(R.string.sync_action_syncing_grade_categories); - apiRequest("Grades/Categories", data -> { - if (data == null) { - r("finish", "GradesCategories"); - return; - } - JsonArray categories = data.get("Categories").getAsJsonArray(); - enableStandardGrades = categories.size() > 0; - profile.putStudentData("enableStandardGrades", enableStandardGrades); - if (!enableStandardGrades) { - r("finish", "GradesCategories"); - return; - } - gradeCategoryListChanged = true; - //d("Got Grades/Categories: "+categories.toString()); - try { - for (JsonElement categoryEl : categories) { - JsonObject category = categoryEl.getAsJsonObject(); - JsonElement name = category.get("Name"); - JsonElement weight = category.get("Weight"); - JsonElement color = category.get("Color"); - JsonElement countToTheAverage = category.get("CountToTheAverage"); - int colorInt = Color.BLUE; - if (!(color instanceof JsonNull) && color != null) { - colorInt = colors.get(color.getAsJsonObject().get("Id").getAsInt()); - } - boolean countToTheAverageBool = !(countToTheAverage instanceof JsonNull) && countToTheAverage != null && countToTheAverage.getAsBoolean(); - int weightInt = weight instanceof JsonNull || weight == null || !countToTheAverageBool ? 0 : weight.getAsInt(); - int categoryId = category.get("Id").getAsInt(); - gradeCategoryList.add(new GradeCategory( - profileId, - categoryId, - weightInt, - colorInt, - name instanceof JsonNull || name == null ? "" : name.getAsString() - )); - } - r("finish", "GradesCategories"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 1954, CODE_OTHER, e, data)); - } - }); - } - - private void getGradesComments() { - callback.onActionStarted(R.string.sync_action_syncing_grade_comments); - apiRequest("Grades/Comments", data -> { - if (data == null) { - r("finish", "GradesComments"); - return; - } - - JsonArray comments = data.get("Comments").getAsJsonArray(); - for (JsonElement commentEl : comments) { - JsonObject comment = commentEl.getAsJsonObject(); - long gradeId = comment.get("Grade").getAsJsonObject().get("Id").getAsLong(); - String text = comment.get("Text").getAsString(); - - for (Grade grade : gradeList) { - if (grade.id == gradeId) { - grade.description = text; - break; - } - } - } - - r("finish", "GradesComments"); - }); - } - - private void getPointGradesCategories() { - if (!fullSync || !enablePointGrades) { - // cancel every not-full sync; no need to download categories again - // or - // if it's a full sync, point grades may have already been disabled in getUnits - r("finish", "PointGradesCategories"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_point_grade_categories); - apiRequest("PointGrades/Categories", data -> { - if (data == null) { - r("finish", "PointGradesCategories"); - return; - } - JsonArray categories = data.get("Categories").getAsJsonArray(); - enablePointGrades = categories.size() > 0; - profile.putStudentData("enablePointGrades", enablePointGrades); - if (!enablePointGrades) { - r("finish", "PointGradesCategories"); - return; - } - gradeCategoryListChanged = true; - //d("Got Grades/Categories: "+categories.toString()); - for (JsonElement categoryEl : categories) { - JsonObject category = categoryEl.getAsJsonObject(); - JsonElement name = category.get("Name"); - JsonElement weight = category.get("Weight"); - JsonElement color = category.get("Color"); - JsonElement countToTheAverage = category.get("CountToTheAverage"); - JsonElement valueFrom = category.get("ValueFrom"); - JsonElement valueTo = category.get("ValueTo"); - int colorInt = Color.BLUE; - if (!(color instanceof JsonNull) && color != null) { - colorInt = colors.get(color.getAsJsonObject().get("Id").getAsInt()); - } - boolean countToTheAverageBool = !(countToTheAverage instanceof JsonNull) && countToTheAverage != null && countToTheAverage.getAsBoolean(); - int weightInt = weight instanceof JsonNull || weight == null || !countToTheAverageBool ? 0 : weight.getAsInt(); - int categoryId = category.get("Id").getAsInt(); - float valueFromFloat = valueFrom.getAsFloat(); - float valueToFloat = valueTo.getAsFloat(); - gradeCategoryList.add( - new GradeCategory( - profileId, - categoryId, - weightInt, - colorInt, - name instanceof JsonNull || name == null ? "" : name.getAsString() - ).setValueRange(valueFromFloat, valueToFloat) - ); - } - r("finish", "PointGradesCategories"); - }); - } - - private void getDescriptiveGradesSkills() { - if (!fullSync || !enableDescriptiveGrades) { - // cancel every not-full sync; no need to download categories again - // or - // if it's a full sync, descriptive grades may have already been disabled in getUnits - r("finish", "DescriptiveGradesCategories"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_descriptive_grade_categories); - apiRequest("DescriptiveTextGrades/Skills", data -> { - if (data == null) { - r("finish", "DescriptiveGradesCategories"); - return; - } - JsonArray categories = data.get("Skills").getAsJsonArray(); - enableDescriptiveGrades = categories.size() > 0; - profile.putStudentData("enableDescriptiveGrades", enableDescriptiveGrades); - if (!enableDescriptiveGrades) { - r("finish", "DescriptiveGradesCategories"); - return; - } - gradeCategoryListChanged = true; - //d("Got Grades/Categories: "+categories.toString()); - for (JsonElement categoryEl : categories) { - JsonObject category = categoryEl.getAsJsonObject(); - JsonElement name = category.get("Name"); - JsonElement color = category.get("Color"); - int colorInt = Color.BLUE; - if (!(color instanceof JsonNull) && color != null) { - colorInt = colors.get(color.getAsJsonObject().get("Id").getAsInt()); - } - int weightInt = -1; - int categoryId = category.get("Id").getAsInt(); - gradeCategoryList.add(new GradeCategory( - profileId, - categoryId, - weightInt, - colorInt, - name instanceof JsonNull || name == null ? "" : name.getAsString() - )); - } - r("finish", "DescriptiveGradesCategories"); - }); - } - - private void getTextGradesCategories() { - if (!fullSync || !enableTextGrades) { - // cancel every not-full sync; no need to download categories again - // or - // if it's a full sync, text grades may have already been disabled in getUnits - r("finish", "TextGradesCategories"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_descriptive_grade_categories); - apiRequest("TextGrades/Categories", data -> { - if (data == null) { - r("finish", "TextGradesCategories"); - return; - } - JsonArray categories = data.get("Categories").getAsJsonArray(); - enableTextGrades = categories.size() > 0; - profile.putStudentData("enableTextGrades", enableTextGrades); - if (!enableTextGrades) { - r("finish", "TextGradesCategories"); - return; - } - gradeCategoryListChanged = true; - //d("Got Grades/Categories: "+categories.toString()); - for (JsonElement categoryEl : categories) { - JsonObject category = categoryEl.getAsJsonObject(); - JsonElement name = category.get("Name"); - JsonElement color = category.get("Color"); - int colorInt = Color.BLUE; - if (!(color instanceof JsonNull) && color != null) { - colorInt = colors.get(color.getAsJsonObject().get("Id").getAsInt()); - } - int weightInt = -1; - int categoryId = category.get("Id").getAsInt(); - gradeCategoryList.add(new GradeCategory( - profileId, - categoryId, - weightInt, - colorInt, - name instanceof JsonNull || name == null ? "" : name.getAsString() - )); - } - r("finish", "TextGradesCategories"); - }); - } - - private void getBehaviourGradesCategories() { - if (!fullSync || !enableBehaviourGrades) { - // cancel every not-full sync; no need to download categories again - // or - // if it's a full sync, descriptive grades may have already been disabled in getUnits - r("finish", "BehaviourGradesCategories"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_behaviour_grade_categories); - apiRequest("BehaviourGrades/Points/Categories", data -> { - if (data == null) { - r("finish", "BehaviourGradesCategories"); - return; - } - JsonArray categories = data.get("Categories").getAsJsonArray(); - enableBehaviourGrades = categories.size() > 0; - profile.putStudentData("enableBehaviourGrades", enableBehaviourGrades); - if (!enableBehaviourGrades) { - r("finish", "BehaviourGradesCategories"); - return; - } - gradeCategoryListChanged = true; - //d("Got Grades/Categories: "+categories.toString()); - for (JsonElement categoryEl : categories) { - JsonObject category = categoryEl.getAsJsonObject(); - JsonElement name = category.get("Name"); - JsonElement valueFrom = category.get("ValueFrom"); - JsonElement valueTo = category.get("ValueTo"); - int colorInt = Color.BLUE; - int categoryId = category.get("Id").getAsInt(); - float valueFromFloat = valueFrom.getAsFloat(); - float valueToFloat = valueTo.getAsFloat(); - gradeCategoryList.add( - new GradeCategory( - profileId, - categoryId, - -1, - colorInt, - name instanceof JsonNull || name == null ? "" : name.getAsString() - ).setValueRange(valueFromFloat, valueToFloat) - ); - } - r("finish", "BehaviourGradesCategories"); - }); - } - - private void getGrades() { - d(TAG, "Grades settings: "+enableStandardGrades+", "+enablePointGrades+", "+enableDescriptiveGrades); - if (!enableStandardGrades && false) { - // cancel only if grades have been disabled before - // TODO do not cancel. this does not show any grades until a full sync happens. wtf - // in KOTLIN api, maybe this will be forced when user synchronises this feature exclusively - r("finish", "Grades"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_grades); - apiRequest("Grades", data -> { - if (data == null) { - r("finish", "Grades"); - return; - } - JsonArray grades = data.get("Grades").getAsJsonArray(); - enableStandardGrades = grades.size() > 0; - profile.putStudentData("enableStandardGrades", enableStandardGrades); - if (!enableStandardGrades) { - r("finish", "Grades"); - return; - } - //d("Got Grades: "+grades.toString()); - for (JsonElement gradeEl : grades) { - JsonObject grade = gradeEl.getAsJsonObject(); - long id = grade.get("Id").getAsLong(); - long teacherId = grade.get("AddedBy").getAsJsonObject().get("Id").getAsLong(); - int semester = grade.get("Semester").getAsInt(); - long categoryId = grade.get("Category").getAsJsonObject().get("Id").getAsLong(); - long subjectId = grade.get("Subject").getAsJsonObject().get("Id").getAsLong(); - String name = grade.get("Grade").getAsString(); - float value = getGradeValue(name); - - String str_date = grade.get("AddDate").getAsString(); - long addedDate = Date.fromIso(str_date); - - float weight = 0.0f; - String category = ""; - int color = -1; - GradeCategory gradeCategory = GradeCategory.search(gradeCategoryList, categoryId); - if (gradeCategory != null) { - weight = gradeCategory.weight; - category = gradeCategory.text; - color = gradeCategory.color; - } - - if (name.equals("-") || name.equals("+") || name.equalsIgnoreCase("np") || name.equalsIgnoreCase("bz")) { - // fix for + and - grades that lower the average - weight = 0; - } - - Grade gradeObject = new Grade( - profileId, - id, - category, - color, - "", - name, - value, - weight, - semester, - teacherId, - subjectId - ); - - if (grade.get("IsConstituent").getAsBoolean()) { - // normal grade - gradeObject.type = TYPE_NORMAL; - } - if (grade.get("IsSemester").getAsBoolean()) { - // semester final - gradeObject.type = (gradeObject.semester == 1 ? TYPE_SEMESTER1_FINAL : TYPE_SEMESTER2_FINAL); - } - else if (grade.get("IsSemesterProposition").getAsBoolean()) { - // semester proposed - gradeObject.type = (gradeObject.semester == 1 ? TYPE_SEMESTER1_PROPOSED : TYPE_SEMESTER2_PROPOSED); - } - else if (grade.get("IsFinal").getAsBoolean()) { - // year final - gradeObject.type = TYPE_YEAR_FINAL; - } - else if (grade.get("IsFinalProposition").getAsBoolean()) { - // year final - gradeObject.type = TYPE_YEAR_PROPOSED; - } - - JsonElement historyEl = grade.get("Improvement"); - if (historyEl != null) { - JsonObject history = historyEl.getAsJsonObject(); - long historicalId = history.get("Id").getAsLong(); - for (Grade historicalGrade: gradeList) { - if (historicalGrade.id != historicalId) - continue; - // historicalGrade - historicalGrade.parentId = gradeObject.id; - if (historicalGrade.name.equals("nb")) { - historicalGrade.weight = 0; - } - break; - } - gradeObject.isImprovement = true; - } - - /*if (RegisterGradeCategory.getById(app.register, registerGrade.categoryId, -1) == null) { - getGradesCategories = true; - }*/ - - gradeList.add(gradeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_GRADE, gradeObject.id, profile.getEmpty(), profile.getEmpty(), addedDate)); - } - r("finish", "Grades"); - }); - } - - private void getPointGrades() { - d(TAG, "Grades settings: "+enableStandardGrades+", "+enablePointGrades+", "+enableDescriptiveGrades); - if (!enablePointGrades) { - // cancel only if grades have been disabled before - r("finish", "PointGrades"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_point_grades); - apiRequest("PointGrades", data -> { - if (data == null) { - r("finish", "PointGrades"); - return; - } - JsonArray grades = data.get("Grades").getAsJsonArray(); - enablePointGrades = grades.size() > 0; - profile.putStudentData("enablePointGrades", enablePointGrades); - if (!enablePointGrades) { - r("finish", "PointGrades"); - return; - } - //d("Got Grades: "+grades.toString()); - for (JsonElement gradeEl : grades) { - JsonObject grade = gradeEl.getAsJsonObject(); - long id = grade.get("Id").getAsLong(); - long teacherId = grade.get("AddedBy").getAsJsonObject().get("Id").getAsLong(); - int semester = grade.get("Semester").getAsInt(); - long categoryId = grade.get("Category").getAsJsonObject().get("Id").getAsLong(); - long subjectId = grade.get("Subject").getAsJsonObject().get("Id").getAsLong(); - String name = grade.get("Grade").getAsString(); - float originalValue = grade.get("GradeValue").getAsFloat(); - - String str_date = grade.get("AddDate").getAsString(); - long addedDate = Date.fromIso(str_date); - - float weight = 0.0f; - String category = ""; - int color = -1; - float value = 0.0f; - float maxPoints = 0.0f; - GradeCategory gradeCategory = GradeCategory.search(gradeCategoryList, categoryId); - if (gradeCategory != null) { - weight = gradeCategory.weight; - category = gradeCategory.text; - color = gradeCategory.color; - maxPoints = gradeCategory.valueTo; - value = originalValue; - } - - Grade gradeObject = new Grade( - profileId, - id, - category, - color, - "", - name, - value, - weight, - semester, - teacherId, - subjectId - ); - gradeObject.type = Grade.TYPE_POINT; - gradeObject.valueMax = maxPoints; - - gradeList.add(gradeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_GRADE, gradeObject.id, profile.getEmpty(), profile.getEmpty(), addedDate)); - } - r("finish", "PointGrades"); - }); - } - - private void getDescriptiveGrades() { - d(TAG, "Grades settings: "+enableStandardGrades+", "+enablePointGrades+", "+enableDescriptiveGrades); - if (!enableDescriptiveGrades && !enableTextGrades) { - // cancel only if grades have been disabled before - r("finish", "DescriptiveGrades"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_descriptive_grades); - apiRequest("BaseTextGrades", data -> { - if (data == null) { - r("finish", "DescriptiveGrades"); - return; - } - JsonArray grades = data.get("Grades").getAsJsonArray(); - int descriptiveGradesCount = 0; - int textGradesCount = 0; - //d("Got Grades: "+grades.toString()); - for (JsonElement gradeEl : grades) { - JsonObject grade = gradeEl.getAsJsonObject(); - long id = grade.get("Id").getAsLong(); - long teacherId = grade.get("AddedBy").getAsJsonObject().get("Id").getAsLong(); - int semester = grade.get("Semester").getAsInt(); - long subjectId = grade.get("Subject").getAsJsonObject().get("Id").getAsLong(); - String description = grade.get("Grade").getAsString(); - - long categoryId = -1; - JsonElement categoryEl = grade.get("Category"); - JsonElement skillEl = grade.get("Skill"); - if (categoryEl != null) { - categoryId = categoryEl.getAsJsonObject().get("Id").getAsLong(); - textGradesCount++; - } - if (skillEl != null) { - categoryId = skillEl.getAsJsonObject().get("Id").getAsLong(); - descriptiveGradesCount++; - } - - String str_date = grade.get("AddDate").getAsString(); - long addedDate = Date.fromIso(str_date); - - String category = ""; - int color = -1; - GradeCategory gradeCategory = GradeCategory.search(gradeCategoryList, categoryId); - if (gradeCategory != null) { - category = gradeCategory.text; - color = gradeCategory.color; - } - - Grade gradeObject = new Grade( - profileId, - id, - category, - color, - description, - " ", - 0.0f, - 0, - semester, - teacherId, - subjectId - ); - gradeObject.type = Grade.TYPE_DESCRIPTIVE; - if (categoryEl != null) { - gradeObject.type = Grade.TYPE_TEXT; - } - if (skillEl != null) { - gradeObject.type = Grade.TYPE_DESCRIPTIVE; - } - - gradeList.add(gradeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_GRADE, gradeObject.id, profile.getEmpty(), profile.getEmpty(), addedDate)); - } - enableDescriptiveGrades = descriptiveGradesCount > 0; - enableTextGrades = textGradesCount > 0; - profile.putStudentData("enableDescriptiveGrades", enableDescriptiveGrades); - profile.putStudentData("enableTextGrades", enableTextGrades); - r("finish", "DescriptiveGrades"); - }); - } - - private void getTextGrades() { - callback.onActionStarted(R.string.sync_action_syncing_descriptive_grades); - apiRequest("DescriptiveGrades", data -> { - if (data == null) { - r("finish", "TextGrades"); - return; - } - JsonArray grades = data.get("Grades").getAsJsonArray(); - //d("Got Grades: "+grades.toString()); - for (JsonElement gradeEl : grades) { - JsonObject grade = gradeEl.getAsJsonObject(); - long id = grade.get("Id").getAsLong(); - long teacherId = grade.get("AddedBy").getAsJsonObject().get("Id").getAsLong(); - int semester = grade.get("Semester").getAsInt(); - long subjectId = grade.get("Subject").getAsJsonObject().get("Id").getAsLong(); - JsonElement map = grade.get("Map"); - JsonElement realGrade = grade.get("RealGradeValue"); - String description = ""; - if (map != null) { - description = map.getAsString(); - } - else if (realGrade != null) { - description = realGrade.getAsString(); - } - - long categoryId = -1; - JsonElement skillEl = grade.get("Skill"); - if (skillEl != null) { - categoryId = skillEl.getAsJsonObject().get("Id").getAsLong(); - } - - String str_date = grade.get("AddDate").getAsString(); - long addedDate = Date.fromIso(str_date); - - String category = ""; - int color = -1; - GradeCategory gradeCategory = GradeCategory.search(gradeCategoryList, categoryId); - if (gradeCategory != null) { - category = gradeCategory.text; - color = gradeCategory.color; - } - - Grade gradeObject = new Grade( - profileId, - id, - category, - color, - "", - description, - 0.0f, - 0, - semester, - teacherId, - subjectId - ); - gradeObject.type = Grade.TYPE_DESCRIPTIVE; - - gradeList.add(gradeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_GRADE, gradeObject.id, profile.getEmpty(), profile.getEmpty(), addedDate)); - } - r("finish", "TextGrades"); - }); - } - - private void getBehaviourGrades() { - d(TAG, "Grades settings: "+enableStandardGrades+", "+enablePointGrades+", "+enableDescriptiveGrades); - if (!enableBehaviourGrades) { - // cancel only if grades have been disabled before - r("finish", "BehaviourGrades"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_behaviour_grades); - apiRequest("BehaviourGrades/Points", data -> { - if (data == null) { - r("finish", "BehaviourGrades"); - return; - } - JsonArray grades = data.get("Grades").getAsJsonArray(); - enableBehaviourGrades = grades.size() > 0; - profile.putStudentData("enableBehaviourGrades", enableBehaviourGrades); - if (!enableBehaviourGrades) { - r("finish", "BehaviourGrades"); - return; - } - //d("Got Grades: "+grades.toString()); - DecimalFormat nameFormat = new DecimalFormat("#.##"); - - Grade gradeStartSemester1 = new Grade( - profileId, - -1, - app.getString(R.string.grade_start_points), - 0xffbdbdbd, - app.getString(R.string.grade_start_points_format, 1), - nameFormat.format(startPointsSemester1), - startPointsSemester1, - -1, - 1, - -1, - 1 - ); - gradeStartSemester1.type = Grade.TYPE_BEHAVIOUR; - Grade gradeStartSemester2 = new Grade( - profileId, - -2, - app.getString(R.string.grade_start_points), - 0xffbdbdbd, - app.getString(R.string.grade_start_points_format, 2), - nameFormat.format(startPointsSemester2), - startPointsSemester2, - -1, - 2, - -1, - 1 - ); - gradeStartSemester2.type = Grade.TYPE_BEHAVIOUR; - - gradeList.add(gradeStartSemester1); - gradeList.add(gradeStartSemester2); - metadataList.add(new Metadata(profileId, Metadata.TYPE_GRADE, -1, true, true, profile.getSemesterStart(1).getInMillis())); - metadataList.add(new Metadata(profileId, Metadata.TYPE_GRADE, -2, true, true, profile.getSemesterStart(2).getInMillis())); - - for (JsonElement gradeEl : grades) { - JsonObject grade = gradeEl.getAsJsonObject(); - long id = grade.get("Id").getAsLong(); - long teacherId = grade.get("AddedBy").getAsJsonObject().get("Id").getAsLong(); - int semester = grade.get("Semester").getAsInt(); - long categoryId = grade.get("Category").getAsJsonObject().get("Id").getAsLong(); - long subjectId = 1; - - float value = 0.0f; - String name = "?"; - JsonElement nameValue; - if ((nameValue = grade.get("Value")) != null) { - value = nameValue.getAsFloat(); - name = value < 0 ? nameFormat.format(value) : "+"+nameFormat.format(value); - } - else if ((nameValue = grade.get("ShortName")) != null) { - name = nameValue.getAsString(); - } - - String str_date = grade.get("AddDate").getAsString(); - long addedDate = Date.fromIso(str_date); - - int color = value > 0 ? 16 : value < 0 ? 26 : 12; - color = colors.get(color); - - String category = ""; - float maxPoints = 0.0f; - GradeCategory gradeCategory = GradeCategory.search(gradeCategoryList, categoryId); - if (gradeCategory != null) { - category = gradeCategory.text; - maxPoints = gradeCategory.valueTo; - } - - Grade gradeObject = new Grade( - profileId, - id, - category, - color, - "", - name, - value, - -1, - semester, - teacherId, - subjectId - ); - gradeObject.type = Grade.TYPE_BEHAVIOUR; - gradeObject.valueMax = maxPoints; - - gradeList.add(gradeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_GRADE, gradeObject.id, profile.getEmpty(), profile.getEmpty(), addedDate)); - } - r("finish", "BehaviourGrades"); - }); - } - - //private boolean eventTypeListChanged = false; - private void getEvents() { - //eventTypeList = app.db.eventTypeDao().getAllNow(profileId); - // eventTypeListChanged = false; - callback.onActionStarted(R.string.sync_action_syncing_events); - apiRequest("HomeWorks", data -> { - if (data == null) { - r("finish", "Events"); - return; - } - JsonArray events = data.get("HomeWorks").getAsJsonArray(); - //d("Got Grades: "+events.toString()); - boolean getCustomTypes = false; - try { - for (JsonElement eventEl : events) { - JsonObject event = eventEl.getAsJsonObject(); - - JsonElement el; - JsonObject obj; - - long id = event.get("Id").getAsLong(); - long teacherId = -1; - long subjectId = -1; - int type = -1; - - if ((el = event.get("CreatedBy")) != null - && (obj = el.getAsJsonObject()) != null - && (el = obj.get("Id")) != null) { - teacherId = el.getAsLong(); - } - if ((el = event.get("Subject")) != null - && (obj = el.getAsJsonObject()) != null - && (el = obj.get("Id")) != null) { - subjectId = el.getAsLong(); - } - String topic = event.get("Content").getAsString(); - - if ((el = event.get("Category")) != null - && (obj = el.getAsJsonObject()) != null - && (el = obj.get("Id")) != null) { - type = el.getAsInt(); - } - /*EventType typeObject = app.db.eventTypeDao().getByIdNow(profileId, type); - if (typeObject == null) { - getCustomTypes = true; - }*/ - - JsonElement myClass = event.get("Class"); - long teamId = myClass == null ? -1 : myClass.getAsJsonObject().get("Id").getAsLong(); - - String str_date = event.get("AddDate").getAsString(); - long addedDate = Date.fromIso(str_date); - - str_date = event.get("Date").getAsString(); - Date eventDate = Date.fromY_m_d(str_date); - - - Time startTime = null; - JsonElement lessonNo; - JsonElement timeFrom; - if (!((lessonNo = event.get("LessonNo")) instanceof JsonNull)) { - Pair timePair = lessonRanges.get(lessonNo.getAsInt()); - if(timePair != null) - startTime = timePair.first; - } - if (startTime == null && !((timeFrom = event.get("TimeFrom")) instanceof JsonNull)) { - startTime = Time.fromH_m(timeFrom.getAsString()); - } - - Event eventObject = new Event( - profileId, - id, - eventDate, - startTime, - topic, - -1, - type, - false, - teacherId, - subjectId, - teamId - ); - - eventList.add(eventObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_EVENT, eventObject.id, profile.getEmpty(), profile.getEmpty(), addedDate)); - } - r("finish", "Events"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 2541, CODE_OTHER, e, data)); - } - }); - } - - private void getCustomTypes() { - if (!fullSync) { - r("finish", "CustomTypes"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_event_categories); - apiRequest("HomeWorks/Categories", data -> { - if (data == null) { - r("finish", "CustomTypes"); - return; - } - JsonArray jCategories = data.get("Categories").getAsJsonArray(); - //d("Got Classrooms: "+jClassrooms.toString()); - try { - for (JsonElement categoryEl : jCategories) { - JsonObject category = categoryEl.getAsJsonObject(); - eventTypeList.add(new EventType(profileId, category.get("Id").getAsInt(), category.get("Name").getAsString(), colors.get(category.get("Color").getAsJsonObject().get("Id").getAsInt()))); - } - r("finish", "CustomTypes"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 2573, CODE_OTHER, e, data)); - } - }); - } - - private void getHomework() { - if (!premium) { - r("finish", "Homework"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_homework); - apiRequest("HomeWorkAssignments", data -> { - if (data == null) { - r("finish", "Homework"); - return; - } - JsonArray homeworkList = data.get("HomeWorkAssignments").getAsJsonArray(); - //d("Got Grades: "+events.toString()); - try { - for (JsonElement homeworkEl : homeworkList) { - JsonObject homework = homeworkEl.getAsJsonObject(); - - JsonElement el; - JsonObject obj; - - long id = homework.get("Id").getAsLong(); - long teacherId = -1; - long subjectId = -1; - - if ((el = homework.get("Teacher")) != null - && (obj = el.getAsJsonObject()) != null - && (el = obj.get("Id")) != null) { - teacherId = el.getAsLong(); - } - - String topic = ""; - try { - topic = homework.get("Topic").getAsString() + "\n"; - topic += homework.get("Text").getAsString(); - } - catch (Exception e) { - e.printStackTrace(); - } - - String str_date = homework.get("Date").getAsString(); - Date addedDate = Date.fromY_m_d(str_date); - - str_date = homework.get("DueDate").getAsString(); - Date eventDate = Date.fromY_m_d(str_date); - - - Time startTime = null; - - Event eventObject = new Event( - profileId, - id, - eventDate, - startTime, - topic, - -1, - -1, - false, - teacherId, - subjectId, - -1 - ); - - eventList.add(eventObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_EVENT, eventObject.id, profile.getEmpty(), profile.getEmpty(), addedDate.getInMillis())); - } - r("finish", "Homework"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 2648, CODE_OTHER, e, data)); - } - }); - } - - private void getLuckyNumbers() { - if (!profile.getLuckyNumberEnabled() || (profile.getLuckyNumberDate() != null && profile.getLuckyNumberDate().getValue() == Date.getToday().getValue())) { - r("finish", "LuckyNumbers"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_lucky_number); - apiRequest("LuckyNumbers", data -> { - if (data == null) { - profile.setLuckyNumberEnabled(false); - } - else { - profile.setLuckyNumber(-1); - profile.setLuckyNumberDate(Date.getToday()); - try { - JsonElement luckyNumberEl = data.get("LuckyNumber"); - if (luckyNumberEl != null) { - JsonObject luckyNumber = luckyNumberEl.getAsJsonObject(); - profile.setLuckyNumber(luckyNumber.get("LuckyNumber").getAsInt()); - profile.setLuckyNumberDate(Date.fromY_m_d(luckyNumber.get("LuckyNumberDay").getAsString())); - } - } catch (Exception e) { - finishWithError(new AppError(TAG, 2678, CODE_OTHER, e, data)); - } - finally { - app.db.luckyNumberDao().add(new LuckyNumber(profileId, profile.getLuckyNumberDate(), profile.getLuckyNumber())); - } - } - r("finish", "LuckyNumbers"); - }); - } - - private void getNotices() { - callback.onActionStarted(R.string.sync_action_syncing_notices); - apiRequest("Notes", data -> { - if (data == null) { - r("finish", "Notices"); - return; - } - try { - JsonArray jNotices = data.get("Notes").getAsJsonArray(); - - for (JsonElement noticeEl : jNotices) { - JsonObject notice = noticeEl.getAsJsonObject(); - - int type = notice.get("Positive").getAsInt(); - switch (type) { - case 0: - type = TYPE_NEGATIVE; - break; - case 1: - type = TYPE_POSITIVE; - break; - case 2: - type = TYPE_NEUTRAL; - break; - } - - long id = notice.get("Id").getAsLong(); - - Date addedDate = Date.fromY_m_d(notice.get("Date").getAsString()); - - int semester = profile.dateToSemester(addedDate); - - JsonElement el; - JsonObject obj; - long teacherId = -1; - if ((el = notice.get("Teacher")) != null - && (obj = el.getAsJsonObject()) != null - && (el = obj.get("Id")) != null) { - teacherId = el.getAsLong(); - } - - Notice noticeObject = new Notice( - profileId, - id, - notice.get("Text").getAsString(), - semester, - type, - teacherId - ); - - noticeList.add(noticeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_NOTICE, noticeObject.id, profile.getEmpty(), profile.getEmpty(), addedDate.getInMillis())); - } - r("finish", "Notices"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 2750, CODE_OTHER, e, data)); - } - }); - } - - private SparseArray> attendanceTypes = new SparseArray<>(); - private void getAttendanceTypes() { - callback.onActionStarted(R.string.sync_action_syncing_attendance_types); - apiRequest("Attendances/Types", data -> { - if (data == null) { - r("finish", "AttendanceTypes"); - return; - } - try { - JsonArray jTypes = data.get("Types").getAsJsonArray(); - for (JsonElement typeEl : jTypes) { - JsonObject type = typeEl.getAsJsonObject(); - int id = type.get("Id").getAsInt(); - attendanceTypes.put(id, - new Pair<>( - type.get("Standard").getAsBoolean() ? id : type.getAsJsonObject("StandardType").get("Id").getAsInt(), - type.get("Name").getAsString() - ) - ); - } - r("finish", "AttendanceTypes"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 2782, CODE_OTHER, e, data)); - } - }); - } - - private void getAttendance() { - callback.onActionStarted(R.string.sync_action_syncing_attendance); - apiRequest("Attendances"+(fullSync ? "" : "?dateFrom="+ Date.getToday().stepForward(0, -1, 0).getStringY_m_d()), data -> { - if (data == null) { - r("finish", "Attendance"); - return; - } - - try { - JsonArray jAttendance = data.get("Attendances").getAsJsonArray(); - - for (JsonElement attendanceEl : jAttendance) { - JsonObject attendance = attendanceEl.getAsJsonObject(); - - int type = attendance.getAsJsonObject("Type").get("Id").getAsInt(); - Pair attendanceType; - if ((attendanceType = attendanceTypes.get(type)) != null) { - type = attendanceType.first; - } - switch (type) { - case 1: - type = TYPE_ABSENT; - break; - case 2: - type = TYPE_BELATED; - break; - case 3: - type = TYPE_ABSENT_EXCUSED; - break; - case 4: - type = TYPE_RELEASED; - break; - default: - case 100: - type = TYPE_PRESENT; - break; - } - - String idStr = attendance.get("Id").getAsString(); - int id = strToInt(idStr.replaceAll("[^\\d.]", "")); - - long addedDate = Date.fromIso(attendance.get("AddDate").getAsString()); - - Time startTime = Time.getNow(); - Pair timePair = lessonRanges.get(attendance.get("LessonNo").getAsInt()); - if (timePair != null) - startTime = timePair.first; - Date lessonDate = Date.fromY_m_d(attendance.get("Date").getAsString()); - int lessonWeekDay = lessonDate.getWeekDay(); - long subjectId = -1; - String topic = ""; - if (attendanceType != null) { - topic = attendanceType.second; - } - - for (Lesson lesson: lessonList) { - if (lesson.weekDay == lessonWeekDay && lesson.startTime.getValue() == startTime.getValue()) { - subjectId = lesson.subjectId; - } - } - - Attendance attendanceObject = new Attendance( - profileId, - id, - attendance.getAsJsonObject("AddedBy").get("Id").getAsLong(), - subjectId, - attendance.get("Semester").getAsInt(), - topic, - lessonDate, - startTime, - type); - - attendanceList.add(attendanceObject); - if (attendanceObject.type != TYPE_PRESENT) { - metadataList.add(new Metadata(profileId, Metadata.TYPE_ATTENDANCE, attendanceObject.id, profile.getEmpty(), profile.getEmpty(), addedDate)); - } - } - r("finish", "Attendance"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 2872, CODE_OTHER, e, data)); - } - }); - } - - private void getAnnouncements() { - callback.onActionStarted(R.string.sync_action_syncing_announcements); - apiRequest("SchoolNotices", data -> { - if (data == null) { - r("finish", "Announcements"); - return; - } - try { - JsonArray jAnnouncements = data.get("SchoolNotices").getAsJsonArray(); - - for (JsonElement announcementEl : jAnnouncements) { - JsonObject announcement = announcementEl.getAsJsonObject(); - - String idStr = announcement.get("Id").getAsString(); - long id = crc16(idStr.getBytes()); - - long addedDate = Date.fromIso(announcement.get("CreationDate").getAsString()); - - boolean read = announcement.get("WasRead").getAsBoolean(); - - String subject = ""; - String text = ""; - Date startDate = null; - Date endDate = null; - try { - subject = announcement.get("Subject").getAsString(); - text = announcement.get("Content").getAsString(); - startDate = Date.fromY_m_d(announcement.get("StartDate").getAsString()); - endDate = Date.fromY_m_d(announcement.get("EndDate").getAsString()); - } - catch (Exception e) { - e.printStackTrace(); - } - - JsonElement el; - JsonObject obj; - long teacherId = -1; - if ((el = announcement.get("AddedBy")) != null - && (obj = el.getAsJsonObject()) != null - && (el = obj.get("Id")) != null) { - teacherId = el.getAsLong(); - } - - Announcement announcementObject = new Announcement( - profileId, - id, - subject, - text, - startDate, - endDate, - teacherId - ); - - announcementList.add(announcementObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_ANNOUNCEMENT, announcementObject.id, read, read, addedDate)); - } - r("finish", "Announcements"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 2944, CODE_OTHER, e, data)); - } - }); - } - - private void getPtMeetings() { - if (!fullSync) { - r("finish", "PtMeetings"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_pt_meetings); - apiRequest("ParentTeacherConferences", data -> { - if (data == null) { - r("finish", "PtMeetings"); - return; - } - try { - JsonArray jMeetings = data.get("ParentTeacherConferences").getAsJsonArray(); - for (JsonElement meetingEl: jMeetings) { - JsonObject meeting = meetingEl.getAsJsonObject(); - - long id = meeting.get("Id").getAsLong(); - Event eventObject = new Event( - profileId, - id, - Date.fromY_m_d(meeting.get("Date").getAsString()), - Time.fromH_m(meeting.get("Time").getAsString()), - meeting.get("Topic").getAsString(), - -1, - TYPE_PT_MEETING, - false, - meeting.getAsJsonObject("Teacher").get("Id").getAsLong(), - -1, - teamClassId - ); - eventList.add(eventObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_EVENT, eventObject.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - r("finish", "PtMeetings"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 2996, CODE_OTHER, e, data)); - } - }); - } - - private SparseArray teacherFreeDaysTypes = new SparseArray<>(); - private void getTeacherFreeDaysTypes() { - if (!fullSync) { - r("finish", "TeacherFreeDaysTypes"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_teacher_free_days_types); - apiRequest("TeacherFreeDays/Types", data -> { - if (data == null) { - r("finish", "TeacherFreeDays"); - return; - } - try { - JsonArray jTypes = data.get("Types").getAsJsonArray(); - for (JsonElement typeEl : jTypes) { - JsonObject type = typeEl.getAsJsonObject(); - int id = type.get("Id").getAsInt(); - teacherFreeDaysTypes.put(id, type.get("Name").getAsString()); - } - r("finish", "TeacherFreeDaysTypes"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 3019, CODE_OTHER, e, data)); - } - }); - } - - private void getTeacherFreeDays() { - callback.onActionStarted(R.string.sync_action_syncing_teacher_free_days); - apiRequest("TeacherFreeDays", data -> { - if (data == null) { - r("finish", "TeacherFreeDays"); - return; - } - try { - JsonArray jFreeDays = data.get("TeacherFreeDays").getAsJsonArray(); - for (JsonElement freeDayEl: jFreeDays) { - JsonObject freeDay = freeDayEl.getAsJsonObject(); - - long id = freeDay.get("Id").getAsLong(); - long teacherId = freeDay.getAsJsonObject("Teacher").get("Id").getAsLong(); - - Date dateFrom = Date.fromY_m_d(freeDay.get("DateFrom").getAsString()); - Date dateTo = Date.fromY_m_d(freeDay.get("DateTo").getAsString()); - - Time timeFrom = null; - Time timeTo = null; - - if (freeDay.get("TimeFrom") != null && freeDay.get("TimeTo") != null) { - timeFrom = Time.fromH_m_s(freeDay.get("TimeFrom").getAsString()); - timeTo = Time.fromH_m_s(freeDay.get("TimeTo").getAsString()); - } - - long type = freeDay.getAsJsonObject("Type").get("Id").getAsLong(); - - //String topic = teacherFreeDaysTypes.get(type)+"\n"+(dateFrom.getValue() != dateTo.getValue() ? dateFrom.getFormattedString()+" - "+dateTo.getFormattedString() : ""); - - TeacherAbsence teacherAbsence = new TeacherAbsence( - profileId, - id, - teacherId, - type, - dateFrom, - dateTo, - timeFrom, - timeTo - ); - - teacherAbsenceList.add(teacherAbsence); - metadataList.add( - new Metadata( - profileId, - Metadata.TYPE_TEACHER_ABSENCE, - teacherAbsence.getId(), - true, - true, - System.currentTimeMillis()) - ); - - } - r("finish", "TeacherFreeDays"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 3069, CODE_OTHER, e, data)); - } - }); - } - - private void getSchoolFreeDays() { - callback.onActionStarted(R.string.sync_action_syncing_school_free_days); - apiRequest("SchoolFreeDays" + (unitId != -1 ? "?unit=" + unitId : ""), data -> { - if (data == null) { - r("finish", "SchoolFreeDays"); - return; - } - try { - JsonArray jFreeDays = data.get("SchoolFreeDays").getAsJsonArray(); - - for (JsonElement freeDayEl: jFreeDays) { - continue; - } - r("finish", "SchoolFreeDays"); - } catch (Exception e) { - finishWithError(new AppError(TAG, 3069, CODE_OTHER, e, data)); - } - }); - } - - private void getMessagesLogin() { - if (synergiaPassword == null) { - // skip messages - r("finish", "MessagesOutbox"); - return; - } - loginSynergia(() -> { - r("finish", "MessagesLogin"); - }); - } - - private void getMessagesInbox() { - String body = - "\n" + - "
\n" + - " \n" + - " 0\n" + - " \n" + - ""; - synergiaRequest("Inbox/action/GetList", body, data -> { - try { - long startTime = System.currentTimeMillis(); - - for (Element e: data.select("response GetList data ArrayItem")) { - - long id = Long.parseLong(e.select("messageId").text()); - - String subject = e.select("topic").text(); - - String senderFirstName = e.select("senderFirstName").text(); - String senderLastName = e.select("senderLastName").text(); - - long senderId = -1; - - for (Teacher teacher: teacherList) { - if (teacher.name.equalsIgnoreCase(senderFirstName) && teacher.surname.equalsIgnoreCase(senderLastName)) { - senderId = teacher.id; - break; - } - } - if (senderId == -1) { - Teacher teacher = new Teacher(profileId, -1 * Utils.crc16((senderFirstName+" "+senderLastName).getBytes()), senderFirstName, senderLastName); - senderId = teacher.id; - teacherList.add(teacher); - teacherListChanged = true; - } - - long readDate = 0; - long sentDate; - - String readDateStr = e.select("readDate").text(); - String sentDateStr = e.select("sendDate").text(); - - DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); - if (!readDateStr.isEmpty()) { - readDate = formatter.parse(readDateStr).getTime(); - } - sentDate = formatter.parse(sentDateStr).getTime(); - - Message message = new Message( - profileId, - id, - subject, - null, - TYPE_RECEIVED, - senderId, - -1 - ); - - MessageRecipient messageRecipient = new MessageRecipient( - profileId, - -1 /* me */, - -1, - readDate, - /*messageId*/ id - ); - - if (!e.select("isAnyFileAttached").text().equals("0")) - message.setHasAttachments(); - - messageList.add(message); - messageRecipientList.add(messageRecipient); - messageMetadataList.add(new Metadata(profileId, Metadata.TYPE_MESSAGE, message.id, readDate > 0, readDate > 0 || profile.getEmpty(), sentDate)); - } - - } catch (Exception e3) { - finishWithError(new AppError(TAG, 3164, CODE_OTHER, e3, data.outerHtml())); - return; - } - - r("finish", "MessagesInbox"); - }); - } - - private void getMessagesOutbox() { - if (!fullSync && onlyFeature != FEATURE_MESSAGES_OUTBOX && !profile.getEmpty()) { - // a quick sync and the profile is already synced at least once - r("finish", "MessagesOutbox"); - return; - } - String body = - "\n" + - "
\n" + - " \n" + - " 0\n" + - " \n" + - ""; - synergiaRequest("Outbox/action/GetList", body, data -> { - try { - long startTime = System.currentTimeMillis(); - - for (Element e: data.select("response GetList data ArrayItem")) { - - long id = Long.parseLong(e.select("messageId").text()); - - String subject = e.select("topic").text(); - - String receiverFirstName = e.select("receiverFirstName").text(); - String receiverLastName = e.select("receiverLastName").text(); - - long receiverId = -1; - - for (Teacher teacher: teacherList) { - if (teacher.name.equalsIgnoreCase(receiverFirstName) && teacher.surname.equalsIgnoreCase(receiverLastName)) { - receiverId = teacher.id; - break; - } - } - if (receiverId == -1) { - Teacher teacher = new Teacher(profileId, -1 * Utils.crc16((receiverFirstName+" "+receiverLastName).getBytes()), receiverFirstName, receiverLastName); - receiverId = teacher.id; - teacherList.add(teacher); - teacherListChanged = true; - } - - long sentDate; - - String sentDateStr = e.select("sendDate").text(); - - DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); - sentDate = formatter.parse(sentDateStr).getTime(); - - Message message = new Message( - profileId, - id, - subject, - null, - TYPE_SENT, - -1, - -1 - ); - - MessageRecipient messageRecipient = new MessageRecipient( - profileId, - receiverId, - -1, - -1, - /*messageId*/ id - ); - - if (!e.select("isAnyFileAttached").text().equals("0")) - message.setHasAttachments(); - - messageList.add(message); - messageRecipientIgnoreList.add(messageRecipient); - metadataList.add(new Metadata(profileId, Metadata.TYPE_MESSAGE, message.id, true, true, sentDate)); - } - - } catch (Exception e3) { - finishWithError(new AppError(TAG, 3270, CODE_OTHER, e3, data.outerHtml())); - return; - } - - r("finish", "MessagesOutbox"); - }); - } - - @Override - public Map getConfigurableEndpoints(Profile profile) { - Map configurableEndpoints = new LinkedHashMap<>(); - configurableEndpoints.put("Classrooms", new Endpoint("Classrooms",true, false, profile.getChangedEndpoints())); - configurableEndpoints.put("Timetables", new Endpoint("Timetables",true, false, profile.getChangedEndpoints())); - configurableEndpoints.put("Substitutions", new Endpoint("Substitutions",true, false, profile.getChangedEndpoints())); - configurableEndpoints.put("Grades", new Endpoint("Grades",true, false, profile.getChangedEndpoints())); - configurableEndpoints.put("PointGrades", new Endpoint("PointGrades",true, false, profile.getChangedEndpoints())); - configurableEndpoints.put("Events", new Endpoint("Events",true, false, profile.getChangedEndpoints())); - configurableEndpoints.put("Homework", new Endpoint("Homework",true, false, profile.getChangedEndpoints())); - configurableEndpoints.put("LuckyNumbers", new Endpoint("LuckyNumbers",true, false, profile.getChangedEndpoints())); - configurableEndpoints.put("Notices", new Endpoint("Notices",true, false, profile.getChangedEndpoints())); - configurableEndpoints.put("Attendance", new Endpoint("Attendance",true, false, profile.getChangedEndpoints())); - configurableEndpoints.put("Announcements", new Endpoint("Announcements",true, true, profile.getChangedEndpoints())); - configurableEndpoints.put("PtMeetings", new Endpoint("PtMeetings",true, true, profile.getChangedEndpoints())); - configurableEndpoints.put("TeacherFreeDays", new Endpoint("TeacherFreeDays",false, false, profile.getChangedEndpoints())); - //configurableEndpoints.put("SchoolFreeDays", new Endpoint("SchoolFreeDays",true, true, profile.changedEndpoints)); - //configurableEndpoints.put("ClassFreeDays", new Endpoint("ClassFreeDays",true, true, profile.changedEndpoints)); - configurableEndpoints.put("MessagesInbox", new Endpoint("MessagesInbox", true, false, profile.getChangedEndpoints())); - configurableEndpoints.put("MessagesOutbox", new Endpoint("MessagesOutbox", true, true, profile.getChangedEndpoints())); - return configurableEndpoints; - } - - @Override - public boolean isEndpointEnabled(Profile profile, boolean defaultActive, String name) { - return defaultActive ^ contains(profile.getChangedEndpoints(), name); - } - - @Override - public void syncMessages(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile) - { - if (!prepare(activityContext, errorCallback, profile.getId(), profile, LoginStore.fromProfileFull(profile))) - return; - - login(() -> { - Librus.this.profile.setEmpty(true); - targetEndpoints = new ArrayList<>(); - targetEndpoints.add("Users"); - targetEndpoints.add("MessagesLogin"); - targetEndpoints.add("MessagesInbox"); - targetEndpoints.add("MessagesOutbox"); - targetEndpoints.add("Finish"); - PROGRESS_COUNT = targetEndpoints.size()-1; - PROGRESS_STEP = (90/PROGRESS_COUNT); - begin(); - }); - } - - /* __ __ - | \/ | - | \ / | ___ ___ ___ __ _ __ _ ___ ___ - | |\/| |/ _ \/ __/ __|/ _` |/ _` |/ _ \/ __| - | | | | __/\__ \__ \ (_| | (_| | __/\__ \ - |_| |_|\___||___/___/\__,_|\__, |\___||___/ - __/ | - |__*/ - @Override - public void getMessage(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull MessageFull message, @NonNull MessageGetCallback messageCallback) - { - if (message.body != null) { - boolean readByAll = true; - // load this message's recipient(s) data - message.recipients = app.db.messageRecipientDao().getAllByMessageId(profile.getId(), message.id); - for (MessageRecipientFull recipient: message.recipients) { - if (recipient.id == -1) - recipient.fullName = profile.getStudentNameLong(); - if (message.type == TYPE_SENT && recipient.readDate < 1) - readByAll = false; - } - if (!message.seen) { - app.db.metadataDao().setSeen(profile.getId(), message, true); - } - if (readByAll) { - // if a sent msg is not read by everyone, download it again to check the read status - new Handler(activityContext.getMainLooper()).post(() -> { - messageCallback.onSuccess(message); - }); - return; - } - } - - if (!prepare(activityContext, errorCallback, profile.getId(), profile, LoginStore.fromProfileFull(profile))) - return; - - loginSynergia(() -> { - String requestBody = - "\n" + - "
\n" + - " \n" + - " "+message.id+"\n" + - " 0\n" + - " \n" + - ""; - synergiaRequest("GetMessage", requestBody, data -> { - - List messageRecipientList = new ArrayList<>(); - - try { - Element e = data.select("response GetMessage data").first(); - - String body = e.select("Message").text(); - body = new String(Base64.decode(body, Base64.DEFAULT)); - body = body.replaceAll("\n", "
"); - body = body.replaceAll("", ""); - - message.clearAttachments(); - Elements attachments = e.select("attachments ArrayItem"); - if (attachments != null) { - for (Element attachment: attachments) { - message.addAttachment(Long.parseLong(attachment.select("id").text()), attachment.select("filename").text(), -1); - } - } - - message.body = body; - - if (message.type == TYPE_RECEIVED) { - app.db.teacherDao().updateLoginId(profileId, message.senderId, e.select("senderId").text()); - - MessageRecipientFull recipient = new MessageRecipientFull(profileId, -1, message.id); - - long readDate = 0; - String readDateStr = e.select("readDate").text(); - if (!readDateStr.isEmpty()) { - DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); - readDate = formatter.parse(readDateStr).getTime(); - } - recipient.readDate = readDate; - - recipient.fullName = profile.getStudentNameLong(); - messageRecipientList.add(recipient); - - } - else if (message.type == TYPE_SENT) { - List teacherList = app.db.teacherDao().getAllNow(profileId); - for (Element receiver: e.select("receivers ArrayItem")) { - String receiverFirstName = e.select("firstName").text(); - String receiverLastName = e.select("lastName").text(); - - long receiverId = -1; - - for (Teacher teacher: teacherList) { - if (teacher.name.equalsIgnoreCase(receiverFirstName) && teacher.surname.equalsIgnoreCase(receiverLastName)) { - receiverId = teacher.id; - break; - } - } - - app.db.teacherDao().updateLoginId(profileId, receiverId, receiver.select("receiverId").text()); - - MessageRecipientFull recipient = new MessageRecipientFull(profileId, receiverId, message.id); - - long readDate = 0; - String readDateStr = e.select("readed").text(); - if (!readDateStr.isEmpty()) { - DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); - readDate = formatter.parse(readDateStr).getTime(); - } - recipient.readDate = readDate; - - recipient.fullName = receiverFirstName+" "+receiverLastName; - messageRecipientList.add(recipient); - } - } - - } - catch (Exception e) { - finishWithError(new AppError(TAG, 795, CODE_OTHER, e, data.outerHtml())); - return; - } - - if (!message.seen) { - app.db.metadataDao().setSeen(profileId, message, true); - } - app.db.messageDao().add(message); - app.db.messageRecipientDao().addAll((List)(List) messageRecipientList); // not addAllIgnore - - message.recipients = messageRecipientList; - - new Handler(activityContext.getMainLooper()).post(() -> { - messageCallback.onSuccess(message); - }); - }); - }); - } - - @Override - public void getAttachment(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull MessageFull message, long attachmentId, @NonNull AttachmentGetCallback attachmentCallback) { - if (!prepare(activityContext, errorCallback, profile.getId(), profile, LoginStore.fromProfileFull(profile))) - return; - - loginSynergia(() -> { - String requestBody = - "\n" + - "
\n" + - " \n" + - " "+attachmentId+"\n" + - " "+message.id+"\n" + - " 0\n" + - " \n" + - ""; - synergiaRequest("GetFileDownloadLink", requestBody, data -> { - String downloadLink = data.select("response GetFileDownloadLink downloadLink").text(); - Matcher keyMatcher = Pattern.compile("singleUseKey=([0-9A-f_]+)").matcher(downloadLink); - if (keyMatcher.find()) { - getAttachmentCheckKeyTries = 0; - getAttachmentCheckKey(keyMatcher.group(1), attachmentCallback); - } - else { - finishWithError(new AppError(TAG, 629, CODE_OTHER, "Błąd pobierania tokenu. Skontaktuj się z twórcą aplikacji.", data.outerHtml())); - } - }); - }); - } - private int getAttachmentCheckKeyTries = 0; - private void getAttachmentCheckKey(String attachmentKey, AttachmentGetCallback attachmentCallback) { - Request.builder() - .url(SYNERGIA_SANDBOX_URL+"CSCheckKey") - .userAgent(synergiaUserAgent) - .addParameter("singleUseKey", attachmentKey) - .post() - .callback(new JsonCallbackHandler() { - @Override - public void onSuccess(JsonObject data, Response response) { - if (data == null) { - finishWithError(new AppError(TAG, 645, AppError.CODE_MAINTENANCE, response)); - return; - } - try { - String status = data.get("status").getAsString(); - if (status.equals("not_downloaded_yet")) { - if (getAttachmentCheckKeyTries++ > 5) { - finishWithError(new AppError(TAG, 658, CODE_OTHER, "Załącznik niedostępny. Przekroczono czas oczekiwania.", response, data)); - return; - } - new Handler(activityContext.getMainLooper()).postDelayed(() -> { - getAttachmentCheckKey(attachmentKey, attachmentCallback); - }, 2000); - } - else if (status.equals("ready")) { - Request.Builder builder = Request.builder() - .url(SYNERGIA_SANDBOX_URL+"CSDownload&singleUseKey="+attachmentKey); - new Handler(activityContext.getMainLooper()).post(() -> { - attachmentCallback.onSuccess(builder); - }); - } - else { - finishWithError(new AppError(TAG, 667, AppError.CODE_ATTACHMENT_NOT_AVAILABLE, response, data)); - } - } - catch (Exception e) { - finishWithError(new AppError(TAG, 671, AppError.CODE_OTHER, response, e, data)); - } - } - - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 677, CODE_OTHER, response, throwable)); - } - }) - .build() - .enqueue(); - } - - @Override - public void getRecipientList(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull RecipientListGetCallback recipientListGetCallback) { - if (!prepare(activityContext, errorCallback, profile.getId(), profile, LoginStore.fromProfileFull(profile))) - return; - - if (System.currentTimeMillis() - profile.getLastReceiversSync() < 24 * 60 * 60 * 1000) { - AsyncTask.execute(() -> { - List teacherList = app.db.teacherDao().getAllNow(profileId); - new Handler(activityContext.getMainLooper()).post(() -> recipientListGetCallback.onSuccess(teacherList)); - }); - return; - } - - loginSynergia(() -> { - String requestBody = - "\n" + - "
\n" + - " \n" + - " 1\n" + - " \n" + - ""; - synergiaRequest("Receivers/action/GetTypes", requestBody, data -> { - - teacherList = app.db.teacherDao().getAllNow(profileId); - - for (Teacher teacher: teacherList) { - teacher.typeDescription = null; // TODO: 2019-06-13 it better - } - - Elements categories = data.select("response GetTypes data list ArrayItem"); - for (Element category: categories) { - String categoryId = category.select("id").text(); - String categoryName = category.select("name").text(); - Elements categoryList = getRecipientCategory(categoryId); - if (categoryList == null) - return; // the error callback is already executed - for (Element item: categoryList) { - if (item.select("list").size() == 1) { - String className = item.select("label").text(); - Elements list = item.select("list ArrayItem"); - for (Element teacher: list) { - updateTeacher(categoryId, Long.parseLong(teacher.select("id").text()), teacher.select("label").text(), categoryName, className); - } - } - else { - updateTeacher(categoryId, Long.parseLong(item.select("id").text()), item.select("label").text(), categoryName, null); - } - } - } - - app.db.teacherDao().addAll(teacherList); - - profile.setLastReceiversSync(System.currentTimeMillis()); - app.db.profileDao().add(profile); - - new Handler(activityContext.getMainLooper()).post(() -> recipientListGetCallback.onSuccess(new ArrayList<>(teacherList))); - }); - }); - } - private Elements getRecipientCategory(String categoryId) { - Response response = null; - try { - String endpoint = "Receivers/action/GetListForType"; - d(TAG, "Requesting "+SYNERGIA_URL+endpoint); - String body = - "\n" + - "
\n" + - " \n" + - " "+categoryId+"\n" + - " \n" + - ""; - response = Request.builder() - .url(SYNERGIA_URL+endpoint) - .userAgent(synergiaUserAgent) - .setTextBody(body, MediaTypeUtils.APPLICATION_XML) - .build().execute(); - if (response.code() != 200) { - finishWithError(new AppError(TAG, 3569, CODE_OTHER, response)); - return null; - } - String data = new TextCallbackHandler().backgroundParser(response); - if (data.contains("error") || data.contains("")) { - finishWithError(new AppError(TAG, 3556, AppError.CODE_MAINTENANCE, response, data)); - return null; - } - Document doc = Jsoup.parse(data, "", Parser.xmlParser()); - return doc.select("response GetListForType data ArrayItem"); - } catch (Exception e) { - finishWithError(new AppError(TAG, 3562, CODE_OTHER, response, e)); - return null; - } - } - private void updateTeacher(String category, long loginId, String nameLastFirst, String typeDescription, String className) { - nameLastFirst = nameLastFirst.replaceAll("\\s+", " "); - int type = TYPE_OTHER; - String position; - switch (category) { - case "tutors": - type = TYPE_EDUCATOR; - break; - case "teachers": - type = TYPE_TEACHER; - break; - case "pedagogue": - type = TYPE_PEDAGOGUE; - break; - case "librarian": - type = TYPE_LIBRARIAN; - break; - case "admin": - type = TYPE_SCHOOL_ADMIN; - break; - case "secretary": - type = TYPE_SECRETARIAT; - break; - case "sadmin": - type = TYPE_SUPER_ADMIN; - break; - case "parentsCouncil": - type = TYPE_PARENTS_COUNCIL; - int index = nameLastFirst.indexOf(" - "); - position = index == -1 ? "" : nameLastFirst.substring(index+3); - nameLastFirst = index == -1 ? nameLastFirst : nameLastFirst.substring(0, index); - typeDescription = bs(className)+bs(": ", position); - break; - case "schoolParentsCouncil": - type = TYPE_SCHOOL_PARENTS_COUNCIL; - index = nameLastFirst.indexOf(" - "); - position = index == -1 ? "" : nameLastFirst.substring(index+3); - nameLastFirst = index == -1 ? nameLastFirst : nameLastFirst.substring(0, index); - typeDescription = bs(position); - break; - case "contactsGroups": - return; - } - Teacher teacher = Teacher.getByFullNameLastFirst(teacherList, nameLastFirst); - if (teacher == null) { - String[] nameParts = nameLastFirst.split(" ", Integer.MAX_VALUE); - teacher = new Teacher(profileId, -1 * Utils.crc16((nameParts.length > 1 ? nameParts[1]+" "+nameParts[0] : nameParts[0]).getBytes()), nameParts.length > 1 ? nameParts[1] : "", nameParts[0]); - teacherList.add(teacher); - } - teacher.loginId = String.valueOf(loginId); - teacher.type = 0; - teacher.setType(type); - if (type == TYPE_OTHER) { - teacher.typeDescription = typeDescription+bs(" ", teacher.typeDescription); - } - else { - teacher.typeDescription = typeDescription; - } - } - - @Override - public MessagesComposeInfo getComposeInfo(@NonNull ProfileFull profile) { - return new MessagesComposeInfo(0, 0, 150, 20000); - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt new file mode 100644 index 00000000..6f73a59a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt @@ -0,0 +1,150 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-20. + */ + +package pl.szczodrzynski.edziennik.data.api + +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.EdudziennikLoginWeb +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.login.IdziennikLoginApi +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.login.IdziennikLoginWeb +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginApi +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginMessages +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginPortal +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginSynergia +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLoginWeb +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.models.LoginMethod + +// librus +// mobidziennik +// idziennik +// vulcan +// mobireg + +const val SYNERGIA_API_ENABLED = true + + + +const val LOGIN_TYPE_IDZIENNIK = 3 + +const val LOGIN_TYPE_TEMPLATE = 21 + +// LOGIN MODES +const val LOGIN_MODE_IDZIENNIK_WEB = 0 + +const val LOGIN_MODE_TEMPLATE_WEB = 0 + +// LOGIN METHODS +const val LOGIN_METHOD_NOT_NEEDED = -1 +const val LOGIN_METHOD_IDZIENNIK_WEB = 100 +const val LOGIN_METHOD_IDZIENNIK_API = 200 +const val LOGIN_METHOD_TEMPLATE_WEB = 100 +const val LOGIN_METHOD_TEMPLATE_API = 200 + +const val LOGIN_TYPE_LIBRUS = 2 +const val LOGIN_MODE_LIBRUS_EMAIL = 0 +const val LOGIN_MODE_LIBRUS_SYNERGIA = 1 +const val LOGIN_MODE_LIBRUS_JST = 2 +const val LOGIN_METHOD_LIBRUS_PORTAL = 100 +const val LOGIN_METHOD_LIBRUS_API = 200 +const val LOGIN_METHOD_LIBRUS_SYNERGIA = 300 +const val LOGIN_METHOD_LIBRUS_MESSAGES = 400 +val librusLoginMethods = listOf( + LoginMethod(LOGIN_TYPE_LIBRUS, LOGIN_METHOD_LIBRUS_PORTAL, LibrusLoginPortal::class.java) + .withIsPossible { _, loginStore -> + loginStore.mode == LOGIN_MODE_LIBRUS_EMAIL + } + .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }, + + LoginMethod(LOGIN_TYPE_LIBRUS, LOGIN_METHOD_LIBRUS_API, LibrusLoginApi::class.java) + .withIsPossible { _, loginStore -> + loginStore.mode != LOGIN_MODE_LIBRUS_SYNERGIA || SYNERGIA_API_ENABLED + } + .withRequiredLoginMethod { _, loginStore -> + if (loginStore.mode == LOGIN_MODE_LIBRUS_EMAIL) LOGIN_METHOD_LIBRUS_PORTAL else LOGIN_METHOD_NOT_NEEDED + }, + + LoginMethod(LOGIN_TYPE_LIBRUS, LOGIN_METHOD_LIBRUS_SYNERGIA, LibrusLoginSynergia::class.java) + .withIsPossible { _, loginStore -> !loginStore.hasLoginData("fakeLogin") } + .withRequiredLoginMethod { profile, _ -> + if (profile?.hasStudentData("accountPassword") == false) LOGIN_METHOD_LIBRUS_API else LOGIN_METHOD_NOT_NEEDED + }, + + LoginMethod(LOGIN_TYPE_LIBRUS, LOGIN_METHOD_LIBRUS_MESSAGES, LibrusLoginMessages::class.java) + .withIsPossible { _, loginStore -> !loginStore.hasLoginData("fakeLogin") } + .withRequiredLoginMethod { profile, _ -> + if (profile?.hasStudentData("accountPassword") == false) LOGIN_METHOD_LIBRUS_SYNERGIA else LOGIN_METHOD_NOT_NEEDED + } +) + +const val LOGIN_TYPE_MOBIDZIENNIK = 1 +const val LOGIN_MODE_MOBIDZIENNIK_WEB = 0 +const val LOGIN_METHOD_MOBIDZIENNIK_WEB = 100 +const val LOGIN_METHOD_MOBIDZIENNIK_API2 = 300 +val mobidziennikLoginMethods = listOf( + LoginMethod(LOGIN_TYPE_MOBIDZIENNIK, LOGIN_METHOD_MOBIDZIENNIK_WEB, MobidziennikLoginWeb::class.java) + .withIsPossible { _, _ -> true } + .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }/*, + + LoginMethod(LOGIN_TYPE_MOBIDZIENNIK, LOGIN_METHOD_MOBIDZIENNIK_API2, MobidziennikLoginApi2::class.java) + .withIsPossible { _, loginStore -> loginStore.hasLoginData("email") } + .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }*/ +) + +const val LOGIN_TYPE_VULCAN = 4 +const val LOGIN_MODE_VULCAN_API = 0 +const val LOGIN_MODE_VULCAN_WEB = 1 +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 +val vulcanLoginMethods = listOf( + /*LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_MAIN, VulcanLoginWebMain::class.java) + .withIsPossible { _, _ -> false } + .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }, + + LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_NEW, VulcanLoginWebNew::class.java) + .withIsPossible { _, _ -> false } + .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_VULCAN_WEB_MAIN }, + + LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_OLD, VulcanLoginWebOld::class.java) + .withIsPossible { _, _ -> false } + .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_VULCAN_WEB_MAIN },*/ + + LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_API, VulcanLoginApi::class.java) + .withIsPossible { _, _ -> true } + .withRequiredLoginMethod { _, loginStore -> + if (loginStore.mode == LOGIN_MODE_VULCAN_WEB) LOGIN_METHOD_VULCAN_WEB_NEW else LOGIN_METHOD_NOT_NEEDED + } +) + +val idziennikLoginMethods = listOf( + LoginMethod(LOGIN_TYPE_IDZIENNIK, LOGIN_METHOD_IDZIENNIK_WEB, IdziennikLoginWeb::class.java) + .withIsPossible { _, _ -> true } + .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }, + + LoginMethod(LOGIN_TYPE_IDZIENNIK, LOGIN_METHOD_IDZIENNIK_API, IdziennikLoginApi::class.java) + .withIsPossible { _, _ -> true } + .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_IDZIENNIK_WEB } +) + +const val LOGIN_TYPE_EDUDZIENNIK = 5 +const val LOGIN_METHOD_EDUDZIENNIK_WEB = 100 +val edudziennikLoginMethods = listOf( + LoginMethod(LOGIN_TYPE_EDUDZIENNIK, LOGIN_METHOD_EDUDZIENNIK_WEB, EdudziennikLoginWeb::class.java) + .withIsPossible { _, _ -> true } + .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED } +) + +val templateLoginMethods = listOf( + LoginMethod(LOGIN_TYPE_TEMPLATE, LOGIN_METHOD_TEMPLATE_WEB, TemplateLoginWeb::class.java) + .withIsPossible { _, _ -> true } + .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }, + + LoginMethod(LOGIN_TYPE_TEMPLATE, LOGIN_METHOD_TEMPLATE_API, TemplateLoginApi::class.java) + .withIsPossible { _, _ -> true } + .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_TEMPLATE_WEB } +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Mobidziennik.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Mobidziennik.java deleted file mode 100644 index 4a3d191f..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Mobidziennik.java +++ /dev/null @@ -1,2426 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api; - -import android.content.Context; -import android.graphics.Color; -import android.os.AsyncTask; -import android.os.Handler; -import android.text.Html; -import android.util.Pair; -import android.util.SparseArray; -import android.util.SparseIntArray; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.crashlytics.android.Crashlytics; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import im.wangchao.mhttp.Request; -import im.wangchao.mhttp.Response; -import im.wangchao.mhttp.callback.TextCallbackHandler; -import pl.szczodrzynski.edziennik.App; -import pl.szczodrzynski.edziennik.BuildConfig; -import pl.szczodrzynski.edziennik.R; -import pl.szczodrzynski.edziennik.data.api.interfaces.AttachmentGetCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface; -import pl.szczodrzynski.edziennik.data.api.interfaces.LoginCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.MessageGetCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.RecipientListGetCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.SyncCallback; -import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance; -import pl.szczodrzynski.edziennik.data.db.modules.events.Event; -import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade; -import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory; -import pl.szczodrzynski.edziennik.data.db.modules.lessons.Lesson; -import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange; -import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore; -import pl.szczodrzynski.edziennik.data.db.modules.luckynumber.LuckyNumber; -import pl.szczodrzynski.edziennik.data.db.modules.messages.Message; -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull; -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient; -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipientFull; -import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata; -import pl.szczodrzynski.edziennik.data.db.modules.notices.Notice; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull; -import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject; -import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher; -import pl.szczodrzynski.edziennik.data.db.modules.teams.Team; -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesComposeInfo; -import pl.szczodrzynski.edziennik.utils.models.Date; -import pl.szczodrzynski.edziennik.utils.models.Endpoint; -import pl.szczodrzynski.edziennik.utils.models.Time; -import pl.szczodrzynski.edziennik.utils.models.Week; - -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_INVALID_LOGIN; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_MAINTENANCE; -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_OTHER; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_ABSENT; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_ABSENT_EXCUSED; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_BELATED; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_CUSTOM; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_PRESENT; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_RELEASED; -import static pl.szczodrzynski.edziennik.data.db.modules.events.Event.TYPE_DEFAULT; -import static pl.szczodrzynski.edziennik.data.db.modules.events.Event.TYPE_EXAM; -import static pl.szczodrzynski.edziennik.data.db.modules.events.Event.TYPE_SHORT_QUIZ; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER1_FINAL; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER1_PROPOSED; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER2_FINAL; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER2_PROPOSED; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_YEAR_FINAL; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_YEAR_PROPOSED; -import static pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_MOBIDZIENNIK; -import static pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_DELETED; -import static pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED; -import static pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT; -import static pl.szczodrzynski.edziennik.utils.Utils.bs; -import static pl.szczodrzynski.edziennik.utils.Utils.crc16; -import static pl.szczodrzynski.edziennik.utils.Utils.d; -import static pl.szczodrzynski.edziennik.utils.Utils.monthFromName; -import static pl.szczodrzynski.edziennik.utils.Utils.strToInt; - -public class Mobidziennik implements EdziennikInterface { - public Mobidziennik(App app) { - this.app = app; - } - - private static final String TAG = "api.Mobidziennik"; - - private App app; - private Context activityContext = null; - private SyncCallback callback = null; - private int profileId = -1; - private Profile profile = null; - private LoginStore loginStore = null; - private boolean fullSync = true; - private Date today = Date.getToday(); - private List targetEndpoints = new ArrayList<>(); - - // PROGRESS - private static final int PROGRESS_LOGIN = 10; - private int PROGRESS_COUNT = 1; - private int PROGRESS_STEP = (90/PROGRESS_COUNT); - - private int onlyFeature = FEATURE_ALL; - - private List teamList; - private List teacherList; - private List subjectList; - private List lessonList; - private List lessonChangeList; - private SparseArray gradeAddedDates; - private SparseArray gradeAverages; - private SparseIntArray gradeColors; - private List gradeList; - private List eventList; - private List noticeList; - private List attendanceList; - private List messageList; - private List messageRecipientList; - private List messageRecipientIgnoreList; - private List metadataList; - private List messageMetadataList; - - private static boolean fakeLogin = false && !(BuildConfig.BUILD_TYPE.equals("release")); - private String lastLogin = null; - private long lastLoginTime = 0; - private String lastResponse = null; - private String loginServerName = null; - private String loginUsername = null; - private String loginPassword = null; - private int studentId = -1; - - private long attendanceLastSync = 0; - - private boolean prepare(@NonNull Context activityContext, @NonNull SyncCallback callback, int profileId, @Nullable Profile profile, @NonNull LoginStore loginStore) { - this.activityContext = activityContext; - this.callback = callback; - this.profileId = profileId; - // here we must have a login store: either with a correct ID or -1 - // there may be no profile and that's when onLoginFirst happens - this.profile = profile; - this.loginStore = loginStore; - this.fullSync = profile == null || profile.getEmpty() || profile.shouldFullSync(activityContext); - this.today = Date.getToday(); - - this.loginServerName = loginStore.getLoginData("serverName", ""); - this.loginUsername = loginStore.getLoginData("username", ""); - this.loginPassword = loginStore.getLoginData("password", ""); - if (loginServerName.equals("") || loginUsername.equals("") || loginPassword.equals("")) { - finishWithError(new AppError(TAG, 157, AppError.CODE_INVALID_LOGIN, "Login field is empty")); - return false; - } - this.studentId = profile == null ? -1 : profile.getStudentData("studentId", -1); - this.attendanceLastSync = profile == null ? 0 : profile.getStudentData("attendanceLastSync", (long)0); - fakeLogin = BuildConfig.DEBUG && loginUsername.toLowerCase().startsWith("fake"); - - teamList = profileId == -1 ? new ArrayList<>() : app.db.teamDao().getAllNow(profileId); - teacherList = profileId == -1 ? new ArrayList<>() : app.db.teacherDao().getAllNow(profileId); - subjectList = new ArrayList<>(); - lessonList = new ArrayList<>(); - lessonChangeList = new ArrayList<>(); - gradeAddedDates = new SparseArray<>(); - gradeAverages = new SparseArray<>(); - gradeColors = new SparseIntArray(); - gradeList = new ArrayList<>(); - eventList = new ArrayList<>(); - noticeList = new ArrayList<>(); - attendanceList = new ArrayList<>(); - messageList = new ArrayList<>(); - messageRecipientList = new ArrayList<>(); - messageRecipientIgnoreList = new ArrayList<>(); - metadataList = new ArrayList<>(); - messageMetadataList = new ArrayList<>(); - - return true; - } - - @Override - public void sync(@NonNull Context activityContext, @NonNull SyncCallback callback, int profileId, @Nullable Profile profile, @NonNull LoginStore loginStore) { - if (!prepare(activityContext, callback, profileId, profile, loginStore)) - return; - - login(() -> { - targetEndpoints = new ArrayList<>(); - targetEndpoints.add("GetData"); - targetEndpoints.add("ProcessData"); - targetEndpoints.add("Attendance"); - targetEndpoints.add("ClassCalendar"); - targetEndpoints.add("GradeDetails"); - targetEndpoints.add("NoticeDetails"); - targetEndpoints.add("Messages"); - targetEndpoints.add("MessagesInbox"); - targetEndpoints.add("Finish"); - PROGRESS_COUNT = targetEndpoints.size()-1; - PROGRESS_STEP = (90/PROGRESS_COUNT); - begin(); - }); - } - @Override - public void syncFeature(@NonNull Context activityContext, @NonNull SyncCallback callback, @NonNull ProfileFull profile, @Nullable int ... featureList) { - if (featureList == null) { - sync(activityContext, callback, profile.getId(), profile, LoginStore.fromProfileFull(profile)); - return; - } - if (!prepare(activityContext, callback, profile.getId(), profile, LoginStore.fromProfileFull(profile))) - return; - - login(() -> { - targetEndpoints = new ArrayList<>(); - if (featureList.length == 1) - onlyFeature = featureList[0]; - if (featureList.length == 1 && (featureList[0] == FEATURE_MESSAGES_INBOX || featureList[0] == FEATURE_MESSAGES_OUTBOX)) { - teacherList = app.db.teacherDao().getAllNow(profileId); - for (Teacher teacher: teacherList) { - teachersMap.put((int) teacher.id, teacher.getFullNameLastFirst()); - } - if (featureList[0] == FEATURE_MESSAGES_INBOX) { - targetEndpoints.add("MessagesInbox"); - } - else { - targetEndpoints.add("Messages"); - } - } - else { - // this is needed for all features except messages - targetEndpoints.add("GetData"); - targetEndpoints.add("ProcessData"); - for (int feature: featureList) { - switch (feature) { - case FEATURE_AGENDA: - targetEndpoints.add("ClassCalendar"); - break; - case FEATURE_GRADES: - targetEndpoints.add("GradeDetails"); - break; - case FEATURE_NOTICES: - targetEndpoints.add("NoticeDetails"); - break; - case FEATURE_ATTENDANCE: - targetEndpoints.add("Attendance"); - break; - case FEATURE_MESSAGES_INBOX: - targetEndpoints.add("MessagesInbox"); - break; - case FEATURE_MESSAGES_OUTBOX: - targetEndpoints.add("Messages"); - break; - } - } - } - targetEndpoints.add("Finish"); - PROGRESS_COUNT = targetEndpoints.size()-1; - PROGRESS_STEP = (90/PROGRESS_COUNT); - begin(); - }); - } - - private void begin() { - callback.onProgress(PROGRESS_LOGIN); - - r("get", null); - } - - private void r(String type, String endpoint) { - // endpoint == null when beginning - if (endpoint == null) - endpoint = targetEndpoints.get(0); - int index = -1; - for (String request: targetEndpoints) { - index++; - if (request.equals(endpoint)) { - break; - } - } - if (type.equals("finish")) { - // called when finishing the action - callback.onProgress(PROGRESS_STEP); - index++; - } - d(TAG, "Called r("+type+", "+endpoint+"). Getting "+targetEndpoints.get(index)); - switch (targetEndpoints.get(index)) { - case "GetData": - getData(); - break; - case "ProcessData": - processData(); - break; - case "ClassCalendar": - getClassCalendar(); - break; - case "GradeDetails": - getGradeDetails(); - break; - case "NoticeDetails": - getNoticeDetails(); - break; - case "Attendance": - getAttendance(); - break; - case "Messages": - getAllMessages(); - break; - case "MessagesInbox": - getMessagesInbox(); - break; - case "Finish": - finish(); - break; - } - } - private void saveData() { - if (teamList.size() > 0) { - app.db.teamDao().clear(profileId); - app.db.teamDao().addAll(teamList); - } - if (teacherList.size() > 0) - app.db.teacherDao().addAllIgnore(teacherList); - if (subjectList.size() > 0) - app.db.subjectDao().addAll(subjectList); - if (lessonList.size() > 0) { - app.db.lessonDao().clear(profileId); - app.db.lessonDao().addAll(lessonList); - } - if (lessonChangeList.size() > 0) - app.db.lessonChangeDao().addAll(lessonChangeList); - if (gradeList.size() > 0) { - app.db.gradeDao().clearForSemester(profileId, profile.getCurrentSemester()); - app.db.gradeDao().addAll(gradeList); - app.db.gradeDao().updateDetails(profileId, gradeAverages, gradeAddedDates, gradeColors); - } - if (eventList.size() > 0) { - app.db.eventDao().removeFuture(profileId, today); - app.db.eventDao().addAll(eventList); - } - if (noticeList.size() > 0) { - app.db.noticeDao().clear(profileId); - app.db.noticeDao().addAll(noticeList); - } - if (attendanceList.size() > 0) { - // clear only last two weeks - //app.db.attendanceDao().clearAfterDate(profileId, Date.getToday().stepForward(0, 0, -14)); - app.db.attendanceDao().addAll(attendanceList); - } - if (messageList.size() > 0) - app.db.messageDao().addAllIgnore(messageList); - if (messageRecipientList.size() > 0) - app.db.messageRecipientDao().addAll(messageRecipientList); - if (messageRecipientIgnoreList.size() > 0) - app.db.messageRecipientDao().addAllIgnore(messageRecipientIgnoreList); - if (metadataList.size() > 0) - app.db.metadataDao().addAllIgnore(metadataList); - if (messageMetadataList.size() > 0) - app.db.metadataDao().setSeen(messageMetadataList); - } - private void finish() { - try { - saveData(); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 303, CODE_OTHER, app.getString(R.string.sync_error_saving_data), null, null, e, null)); - } - if (fullSync) { - profile.setLastFullSync(System.currentTimeMillis()); - fullSync = false; - } - profile.setEmpty(false); - callback.onSuccess(activityContext, new ProfileFull(profile, loginStore)); - } - private void finishWithError(AppError error) { - try { - saveData(); - } - catch (Exception e) { - Crashlytics.logException(e); - } - callback.onError(activityContext, error); - } - - /* _ _ - | | (_) - | | ___ __ _ _ _ __ - | | / _ \ / _` | | '_ \ - | |___| (_) | (_| | | | | | - |______\___/ \__, |_|_| |_| - __/ | - |__*/ - private void login(@NonNull LoginCallback loginCallback) { - if (System.currentTimeMillis() - lastLoginTime < 10*60*1000 - && lastLogin.equals(loginServerName+":"+loginUsername)) { - loginCallback.onSuccess(); - return; - } - app.cookieJar.clearForDomain(loginServerName+".mobidziennik.pl"); - - Request.builder() - .url(fakeLogin ? "https://szkolny.eu/mobimobi/mobi_mod_log.php" : "https://"+loginServerName+".mobidziennik.pl/api/") - .userAgent(System.getProperty( "http.agent" )) - .contentType("application/x-www-form-urlencoded; charset=UTF-8") - .addParameter("wersja", "20") - .addParameter("ip", app.deviceId) - .addParameter("login", loginUsername) - .addParameter("haslo", loginPassword) - .addParameter("token", app.appConfig.fcmTokens.get(LOGIN_TYPE_MOBIDZIENNIK).first) - .post() - .callback(new TextCallbackHandler() { - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 333, CODE_OTHER, response, throwable)); - } - - @Override - public void onSuccess(String data, Response response) { - - //app.profile.loggedIn = (data != null && data.equals("ok")); - if (data == null || data.equals("")) { // for safety - finishWithError(new AppError(TAG, 339, CODE_MAINTENANCE, response)); - } - else if (data.equals("Nie jestes zalogowany")) { - finishWithError(new AppError(TAG, 343, AppError.CODE_INVALID_LOGIN, response, data)); - } - else if (data.equals("ifun")) { - finishWithError(new AppError(TAG, 346, AppError.CODE_INVALID_DEVICE, response, data)); - } - else if (data.equals("stare haslo")) { - finishWithError(new AppError(TAG, 349, AppError.CODE_OLD_PASSWORD, response, data)); - } - else if (data.equals("Archiwum")) { - finishWithError(new AppError(TAG, 352, AppError.CODE_ARCHIVED, response, data)); - } - else if (data.equals("Trwają prace techniczne lub pojawił się jakiś problem")) { - finishWithError(new AppError(TAG, 355, CODE_MAINTENANCE, data, response, data)); - } - else if (data.equals("ok")) - { - // a successful login - if (profile != null && studentId != -1) { - lastLogin = loginServerName+":"+loginUsername; - lastLoginTime = System.currentTimeMillis(); - loginCallback.onSuccess(); - return; - } - getData((data1 -> { - String[] tables = data1.split("T@B#LA"); - - List studentIds = new ArrayList<>(); - List studentNamesLong = new ArrayList<>(); - List studentNamesShort = new ArrayList<>(); - String[] student = tables[8].split("\n"); - for (String aStudent : student) { - if (aStudent.isEmpty()) { - continue; - } - String[] student1 = aStudent.split("\\|", Integer.MAX_VALUE); - if (student1.length == 2) - continue; - studentIds.add(strToInt(student1[0])); - studentNamesLong.add(student1[2] + " " + student1[4]); - studentNamesShort.add(student1[2] + " " + student1[4].charAt(0) + "."); - } - - List profileList = new ArrayList<>(); - - for (int index = 0; index < studentIds.size(); index++) { - Profile profile = new Profile(); - profile.setStudentNameLong(studentNamesLong.get(index)); - profile.setStudentNameShort(studentNamesShort.get(index)); - profile.setName(profile.getStudentNameLong()); - profile.setSubname(loginUsername); - profile.setEmpty(true); - profile.setLoggedIn(true); - profile.putStudentData("studentId", studentIds.get(index)); - profileList.add(profile); - } - - callback.onLoginFirst(profileList, loginStore); - })); - } - else { - if (data.contains("Uuuups... nieprawidłowy adres")) { - finishWithError(new AppError(TAG, 404, AppError.CODE_INVALID_SERVER_ADDRESS, response, data)); - return; - } - finishWithError(new AppError(TAG, 407, AppError.CODE_MAINTENANCE, response, data)); - } - } - }) - .build() - .enqueue(); - } - - /* _ _ _ _ _ _ _ - | | | | | | ___ | | | | | | - | |__| | ___| |_ __ ___ _ __ ___ ( _ ) ___ __ _| | | |__ __ _ ___| | _____ - | __ |/ _ \ | '_ \ / _ \ '__/ __| / _ \/\ / __/ _` | | | '_ \ / _` |/ __| |/ / __| - | | | | __/ | |_) | __/ | \__ \ | (_> < | (_| (_| | | | |_) | (_| | (__| <\__ \ - |_| |_|\___|_| .__/ \___|_| |___/ \___/\/ \___\__,_|_|_|_.__/ \__,_|\___|_|\_\___/ - | | - |*/ - private interface GetDataCallback { - void onSuccess(String data); - } - private void getData(GetDataCallback getDataCallback) { - callback.onActionStarted(R.string.sync_action_syncing); - Request.builder() - .url(fakeLogin ? "https://szkolny.eu/mobimobi/mobi_mod.php?"+System.currentTimeMillis() : "https://"+ loginServerName +".mobidziennik.pl/api/zrzutbazy") - .userAgent(System.getProperty( "http.agent" )) - .callback(new TextCallbackHandler() { - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 427, CODE_OTHER, response, throwable)); - } - - @Override - public void onSuccess(String data, Response response) { - if (data == null || data.equals("")) { - finishWithError(new AppError(TAG, 432, CODE_MAINTENANCE, response)); - return; - } - if (data.equals("Nie jestes zalogowany")) { - finishWithError(new AppError(TAG, 437, CODE_INVALID_LOGIN, response, data)); - lastLoginTime = 0; - return; - } - - lastResponse = data; - - getDataCallback.onSuccess(data); - } - }) - .build() - .enqueue(); - } - private void getData() { - getData((data -> r("finish", "GetData"))); - } - - /* _____ _ _____ _ - | __ \ | | | __ \ | | - | | | | __ _| |_ __ _ | |__) |___ __ _ _ _ ___ ___| |_ ___ - | | | |/ _` | __/ _` | | _ // _ \/ _` | | | |/ _ \/ __| __/ __| - | |__| | (_| | || (_| | | | \ \ __/ (_| | |_| | __/\__ \ |_\__ \ - |_____/ \__,_|\__\__,_| |_| \_\___|\__, |\__,_|\___||___/\__|___/ - | | - |*/ - private void processData() { - callback.onActionStarted(R.string.sync_action_processing_data); - String[] tables = lastResponse.split("T@B#LA"); - - if (studentId == -1) { - return; - } - try { - int i = 0; - for (String table : tables) { - if (i == 0) { - processUsers(table); - } - if (i == 3) { - processDates(table); - } - if (i == 4) { - processSubjects(table); - } - if (i == 7) { - processTeams(table, null); - } - if (i == 8) { - processStudent(table, loginUsername); - } - if (i == 9) { - processTeams(null, table); - } - if (i == 14) { - processGradeCategories(table); - } - if (i == 15) { - processLessons(table); - } - if (i == 16) { - processAttendance(table); - } - if (i == 17) { - processNotices(table); - } - if (i == 18) { - processGrades(table); - } - if (i == 21) { - processEvents(table); - } - if (i == 23) { - processHomework(table); - } - if (i == 24) { - processTimetable(table); - } - i++; - } - r("finish", "ProcessData"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 511, CODE_OTHER, e, lastResponse)); - } - } - - private void getClassCalendar() { - callback.onActionStarted(R.string.sync_action_syncing_calendar); - Request.builder() - .url(fakeLogin ? "https://szkolny.eu/mobimobi/mobi_mod_kalendarzklasowy.php" : "https://" + loginServerName + ".mobidziennik.pl/dziennik/kalendarzklasowy") - .userAgent(System.getProperty("http.agent")) - .callback(new TextCallbackHandler() { - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 523, CODE_OTHER, response, throwable)); - } - - @Override - public void onSuccess(String data, Response response) { - // just skip any failures here - if (data == null || data.equals("")) { - r("finish", "ClassCalendar"); - return; - } - if (data.contains("nie-pamietam-hasla")) { - r("finish", "ClassCalendar"); - return; - } - - if (profile.getLuckyNumberEnabled() - && (profile.getLuckyNumberDate() == null || profile.getLuckyNumberDate().getValue() != Date.getToday().getValue() || profile.getLuckyNumber() == -1)) { - // set the current date as we checked the lucky number today - profile.setLuckyNumber(-1); - profile.setLuckyNumberDate(Date.getToday()); - - Matcher matcher = Pattern.compile("class=\"szczesliwy_numerek\".*>0*([0-9]+)(?:/0*[0-9]+)*", Pattern.DOTALL).matcher(data); - if (matcher.find()) { - try { - profile.setLuckyNumber(Integer.parseInt(matcher.group(1))); - } catch (Exception e3) { - e3.printStackTrace(); - } - } - app.db.luckyNumberDao().add(new LuckyNumber(profileId, profile.getLuckyNumberDate(), profile.getLuckyNumber())); - } - - Matcher matcher = Pattern.compile("events: (.+),$", Pattern.MULTILINE).matcher(data); - if (matcher.find()) { - try { - //d(TAG, matcher.group(1)); - JsonArray events = new JsonParser().parse(matcher.group(1)).getAsJsonArray(); - //d(TAG, "Events size "+events.size()); - for (JsonElement eventEl: events) { - JsonObject event = eventEl.getAsJsonObject(); - - //d(TAG, "Event "+event.toString()); - - JsonElement idEl = event.get("id"); - String idStr; - if (idEl != null && (idStr = idEl.getAsString()).startsWith("kalendarz;")) { - String[] idParts = idStr.split(";", Integer.MAX_VALUE); - if (idParts.length > 2) { - long id = idParts[2].isEmpty() ? -1 : strToInt(idParts[2]); - long targetStudentId = strToInt(idParts[1]); - if (targetStudentId != studentId) - continue; - - // TODO null-safe getAs..() - Date eventDate = Date.fromY_m_d(event.get("start").getAsString()); - //if (eventDate.getValue() < today.getValue()) - // continue; - - String eventColor = event.get("color").getAsString(); - - int eventType = Event.TYPE_INFORMATION; - - switch (eventColor) { - case "#C54449": - case "#c54449": - eventType = Event.TYPE_SHORT_QUIZ; - break; - case "#AB0001": - case "#ab0001": - eventType = Event.TYPE_EXAM; - break; - case "#008928": - eventType = Event.TYPE_CLASS_EVENT; - break; - case "#b66000": - case "#B66000": - eventType = Event.TYPE_EXCURSION; - break; - } - - String title = event.get("title").getAsString(); - String comment = event.get("comment").getAsString(); - - String topic = title; - if (!title.equals(comment)) { - topic += "\n"+comment; - } - - if (id == -1) { - id = crc16(topic.getBytes()); - } - - Event eventObject = new Event( - profileId, - id, - eventDate, - null, - topic, - -1, - eventType, - false, - -1, - -1, - teamClass != null ? teamClass.id : -1 - ); - - - //d(TAG, "Event "+eventObject); - eventList.add(eventObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_EVENT, eventObject.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis() /* no addedDate here though */)); - } - } - } - r("finish", "ClassCalendar"); - } catch (Exception e3) { - finishWithError(new AppError(TAG, 638, CODE_OTHER, response, e3, data)); - } - } - else { - r("finish", "ClassCalendar"); - } - } - }) - .build() - .enqueue(); - } - - private void getGradeDetails() { - callback.onActionStarted(R.string.sync_action_syncing_grade_details); - Request.builder() - .url(fakeLogin ? "https://szkolny.eu/mobimobi/mobi_mod_oceny.php" : "https://" + loginServerName + ".mobidziennik.pl/dziennik/oceny?semestr="+ profile.getCurrentSemester()) - .userAgent(System.getProperty("http.agent")) - .callback(new TextCallbackHandler() { - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 658, CODE_OTHER, response, throwable)); - } - - @Override - public void onSuccess(String data, Response response) { - // just skip any failures here - if (data == null || data.equals("")) { - r("finish", "GradeDetails"); - return; - } - if (data.contains("nie-pamietam-hasla")) { - r("finish", "GradeDetails"); - return; - } - - try { - Document doc = Jsoup.parse(data); - - Elements grades = doc.select("table.spis a, table.spis span, table.spis div"); - - String gradeCategory = ""; - int gradeColor = -1; - String subjectName = ""; - - for (Element e: grades) { - switch (e.tagName()) { - case "div": { - //d(TAG, "Outer HTML "+e.outerHtml()); - Matcher matcher = Pattern.compile("\\n*\\s*(.+?)\\s*\\n*(?:<.*?)??", Pattern.DOTALL).matcher(e.outerHtml()); - if (matcher.find()) { - subjectName = matcher.group(1); - } - break; - } - case "span": { - String css = e.attr("style"); - Matcher matcher = Pattern.compile("background-color:([#A-Fa-f0-9]+);", Pattern.DOTALL).matcher(css); - if (matcher.find()) { - gradeColor = Color.parseColor(matcher.group(1)); - } - matcher = Pattern.compile("> (.+?):", Pattern.DOTALL).matcher(e.outerHtml()); - if (matcher.find()) { - gradeCategory = matcher.group(1); - } - break; - } - case "a": { - int gradeId = strToInt(e.attr("rel")); - float gradeClassAverage = -1; - Date gradeAddedDate = null; - long gradeAddedDateMillis = -1; - - String html = e.html(); - Matcher matcher = Pattern.compile("Średnia ocen:.*([0-9]*\\.?[0-9]*)", Pattern.DOTALL).matcher(html); - if (matcher.find()) { - gradeClassAverage = Float.parseFloat(matcher.group(1)); - } - - matcher = Pattern.compile("Wpisano:.*.+?,\\s([0-9]+)\\s(.+?)\\s([0-9]{4}),\\sgodzina\\s([0-9:]+)", Pattern.DOTALL).matcher(html); - int month = 1; - if (matcher.find()) { - switch (matcher.group(2)) { - case "stycznia": - month = 1; - break; - case "lutego": - month = 2; - break; - case "marca": - month = 3; - break; - case "kwietnia": - month = 4; - break; - case "maja": - month = 5; - break; - case "czerwca": - month = 6; - break; - case "lipca": - month = 7; - break; - case "sierpnia": - month = 8; - break; - case "września": - month = 9; - break; - case "października": - month = 10; - break; - case "listopada": - month = 11; - break; - case "grudnia": - month = 12; - break; - } - gradeAddedDate = new Date( - strToInt(matcher.group(3)), - month, - strToInt(matcher.group(1)) - ); - Time time = Time.fromH_m_s( - matcher.group(4) - ); - gradeAddedDateMillis = gradeAddedDate.combineWith(time); - } - - matcher = Pattern.compile("Liczona do średniej:.*?nie
", Pattern.DOTALL).matcher(html); - if (matcher.find()) { - matcher = Pattern.compile("(.+?).*?.+?.*?\\((.+?)\\).*?.*?Wartość oceny:.*?([0-9.]+).*?Wpisał\\(a\\):.*?(.+?)", Pattern.DOTALL).matcher(html); - if (matcher.find()) { - String gradeName = matcher.group(1); - String gradeDescription = matcher.group(2); - float gradeValue = Float.parseFloat(matcher.group(3)); - String teacherName = matcher.group(4); - - long teacherId = -1; - long subjectId = -1; - - //d(TAG, "Grade "+gradeName+" "+gradeCategory+" subject "+subjectName+" teacher "+teacherName); - - if (!subjectName.equals("")) { - int subjectIndex = -1; - for (int i = 0; i < subjectsMap.size(); i++) { - if (subjectsMap.valueAt(i).equals(subjectName)) { - subjectIndex = i; - } - } - //d(TAG, "subjectIndex "+subjectIndex); - if (subjectIndex >= 0 && subjectIndex < subjectsMap.size()) { - subjectId = subjectsMap.keyAt(subjectIndex); - } - } - - if (!teacherName.equals("")) { - //d(TAG, "teacherName "+teacherName); - int teacherIndex = -1; - for (int i = 0; i < teachersMap.size(); i++) { - if (teachersMap.valueAt(i).equals(teacherName)) { - teacherIndex = i; - } - } - //d(TAG, "teacherIndex "+teacherIndex); - if (teacherIndex >= 0 && teacherIndex < teachersMap.size()) { - teacherId = teachersMap.keyAt(teacherIndex); - } - } - - int semester = profile.dateToSemester(gradeAddedDate); - - Grade gradeObject = new Grade( - profileId, - gradeId, - gradeCategory, - gradeColor, - "NLDŚR, "+gradeDescription, - gradeName, - gradeValue, - 0, - semester, - teacherId, - subjectId - ); - - gradeObject.classAverage = gradeClassAverage; - - gradeList.add(gradeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_GRADE, gradeObject.id, profile.getEmpty(), profile.getEmpty(), gradeAddedDateMillis)); - } - } else { - gradeAverages.put(gradeId, gradeClassAverage); - gradeAddedDates.put(gradeId, gradeAddedDateMillis); - gradeColors.put(gradeId, gradeColor); - } - break; - } - } - } - - r("finish", "GradeDetails"); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 852, CODE_OTHER, response, e, data)); - } - } - }) - .build() - .enqueue(); - } - - // TODO: 2019-06-04 punkty z zachowania znikają po synchronizacji - private void getNoticeDetails() { - if (noticeList.size() == 0) { - r("finish", "NoticeDetails"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_notice_details); - Request.builder() - .url(fakeLogin ? "https://szkolny.eu/mobimobi/mobi_mod_zachowanie.php" : "https://" + loginServerName + ".mobidziennik.pl/mobile/zachowanie") - .userAgent(System.getProperty("http.agent")) - .callback(new TextCallbackHandler() { - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 873, CODE_OTHER, response, throwable)); - } - - @Override - public void onSuccess(String data, Response response) { - // just skip any failures here - if (data == null || data.equals("")) { - r("finish", "NoticeDetails"); - return; - } - if (data.contains("nie-pamietam-hasla")) { - r("finish", "NoticeDetails"); - return; - } - - Matcher matcher = Pattern.compile("(?:([0-9-.,]+).+?)?
.+?Treść:.+?(?:Kategoria: (.+?).+?)?Czas", Pattern.DOTALL).matcher(data); - while (matcher.find()) { - try { - String pointsStr = matcher.group(1); - String idStr = matcher.group(2); - String categoryStr = matcher.groupCount() == 3 ? matcher.group(3) : null; - float points = pointsStr == null ? 0 : Float.parseFloat(pointsStr); - long id = Long.parseLong(idStr); - for (Notice notice: noticeList) { - if (notice.id != id) - continue; - notice.points = points; - notice.category = categoryStr; - } - } catch (Exception e) { - Crashlytics.logException(e); - e.printStackTrace(); - if (onlyFeature == FEATURE_NOTICES) - finishWithError(new AppError(TAG, 880, CODE_OTHER, response, e, data)); - else - r("finish", "NoticeDetails"); - return; - } - } - r("finish", "NoticeDetails"); - } - }) - .build() - .enqueue(); - } - - public static class AttendanceLessonRange { - int weekDay; - int lessonNumber; - Time startTime; - Time endTime; - List lessons; - - public AttendanceLessonRange(int section, int number, int lessonNumber, Time startTime, Time endTime) { - this.weekDay = section == 1 ? number + 4 : number; - this.lessonNumber = lessonNumber; - this.startTime = startTime; - this.endTime = endTime; - this.lessons = new ArrayList<>(); - } - public void addLesson(AttendanceLesson lesson) { - lessons.add(lesson); - } - } - public static class AttendanceLesson { - String subjectName; - String topic; - String teamName; - String teacherName; - long lessonId; - - public AttendanceLesson(String subjectName, String topic, String teamName, String teacherName, long lessonId) { - this.subjectName = subjectName; - this.topic = topic; - this.teamName = teamName; - this.teacherName = teacherName; - this.lessonId = lessonId; - } - } - private Date attendanceCheckDate = Week.getWeekStart(); - private void getAttendance() { - r("finish", "Attendance"); - // TODO: 2019-09-10 please download attendance from /dziennik/frekwencja. /mobile does not work above v13.0 - if (true) { - return; - } - callback.onActionStarted(R.string.sync_action_syncing_attendance); - d(TAG, "Get attendance for week "+ attendanceCheckDate.getStringY_m_d()); - Request.builder() - .url(fakeLogin ? "https://szkolny.eu/mobimobi/mobi_mod_frekwencja.php" : "https://" + loginServerName + ".mobidziennik.pl/mobile/frekwencja") - .userAgent(System.getProperty("http.agent")) - .addParameter("uczen", studentId) - .addParameter("data_poniedzialek", attendanceCheckDate.getStringY_m_d()) - .post() - .callback(new TextCallbackHandler() { - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 944, CODE_OTHER, response, throwable)); - } - - @Override - public void onSuccess(String data, Response response) { - // just skip any failures here - if (data == null || data.equals("")) { - r("finish", "Attendance"); - return; - } - if (data.contains("nie-pamietam-hasla")) { - r("finish", "Attendance"); - return; - } - - List ranges = new ArrayList<>(); - - try { - Matcher matcher = Pattern.compile("
.*?

([0-9]{2}):([0-9]{2}) - ([0-9]{2}):([0-9]{2}) (.+?)\\s*?

.*?

", Pattern.DOTALL).matcher(data); - while (matcher.find()) { - int section = strToInt(matcher.group(1)); - int number = strToInt(matcher.group(2)); - int lessonNumber = strToInt(matcher.group(3)); - int startHour = strToInt(matcher.group(4)); - int startMinute = strToInt(matcher.group(5)); - int endHour = strToInt(matcher.group(6)); - int endMinute = strToInt(matcher.group(7)); - String lessonsString = matcher.group(8); - AttendanceLessonRange range = new AttendanceLessonRange(section, number, lessonNumber, new Time(startHour, startMinute, 0), new Time(endHour, endMinute, 0)); - Matcher matcher2 = Pattern.compile("(.+?) - (.+?)??.+?.+?\\(([0-9]{1,2}) (.+?),\\s+(.+?), id lekcji: ([0-9]+?)\\).+?", Pattern.DOTALL).matcher(lessonsString); - while (matcher2.find()) { - String topic = matcher2.group(2); - topic = topic == null ? "" : Html.fromHtml(topic).toString(); - if (topic.startsWith("Lekcja odwołana:")) - continue; - String teamName = matcher2.group(3)+matcher2.group(4); - boolean notFound = true; - for (Team team: teamList) { - if (teamName.equals(team.name)) { - notFound = false; - break; - } - } - if (notFound) - continue; - range.addLesson( - new AttendanceLesson( - matcher2.group(1), - topic, - teamName, - matcher2.group(5), - strToInt(matcher2.group(6)) - ) - ); - } - ranges.add(range); - } - matcher = Pattern.compile("([.|sz+]+)?", Pattern.DOTALL).matcher(data); - int index = 0; - while (matcher.find()) { - int currentIndex = index++; - String markers = matcher.groupCount() == 1 ? matcher.group(1) : null; - if (markers == null) - continue; - AttendanceLessonRange range = ranges.get(currentIndex); - - Date date = attendanceCheckDate.clone().stepForward(0, 0, range.weekDay); - long addedDate = date.combineWith(range.startTime); - - int markerIndex = 0; - for (char marker: markers.toCharArray()) { - int type = TYPE_CUSTOM; - switch (marker) { - case '.': - type = TYPE_PRESENT; - break; - case '|': - type = TYPE_ABSENT; - break; - case 's': - type = TYPE_BELATED; - break; - case 'z': - type = TYPE_RELEASED; - break; - case '+': - type = TYPE_ABSENT_EXCUSED; - } - AttendanceLesson lesson; - if (markerIndex >= range.lessons.size()) { - lesson = new AttendanceLesson("", "Nieznana lekcja", "???", "", addedDate); - } - else { - lesson = range.lessons.get(markerIndex); - } - - long teacherId = -1; - long subjectId = -1; - - int teacherIndex = -1; - for (int i = 0; i < teachersMap.size(); i++) { - if (teachersMap.valueAt(i).equals(lesson.teacherName)) { - teacherIndex = i; - } - } - if (teacherIndex >= 0 && teacherIndex < teachersMap.size()) { - teacherId = teachersMap.keyAt(teacherIndex); - } - - int subjectIndex = -1; - for (int i = 0; i < subjectsMap.size(); i++) { - if (subjectsMap.valueAt(i).equals(lesson.subjectName)) { - subjectIndex = i; - } - } - if (subjectIndex >= 0 && subjectIndex < subjectsMap.size()) { - subjectId = subjectsMap.keyAt(subjectIndex); - } - - Attendance attendanceObject = new Attendance( - profileId, - lesson.lessonId, - teacherId, - subjectId, - profile.dateToSemester(date), - lesson.topic, - date, - range.startTime, - type); - markerIndex++; - attendanceList.add(attendanceObject); - if (attendanceObject.type != TYPE_PRESENT) { - boolean markAsRead = onlyFeature == FEATURE_ATTENDANCE && attendanceLastSync == 0; - metadataList.add(new Metadata(profileId, Metadata.TYPE_ATTENDANCE, attendanceObject.id, profile.getEmpty() || markAsRead, profile.getEmpty() || markAsRead, addedDate)); - } - } - } - } catch (Exception e) { - Crashlytics.logException(e); - e.printStackTrace(); - if (onlyFeature == FEATURE_ATTENDANCE) - finishWithError(new AppError(TAG, 955, CODE_OTHER, response, e, data)); - else - r("finish", "Attendance"); - return; - } - - if (onlyFeature == FEATURE_ATTENDANCE) { - // syncing attendance exclusively - if (attendanceLastSync == 0) { - // first sync - get attendance until it's start of the school year - attendanceLastSync = profile.getSemesterStart(1).getInMillis(); - } - Date lastSyncDate = Date.fromMillis(attendanceLastSync); - lastSyncDate.stepForward(0, 0, -7); - if (lastSyncDate.getValue() < attendanceCheckDate.getValue()) { - attendanceCheckDate.stepForward(0, 0, -7); - r("get", "Attendance"); - } - else { - profile.putStudentData("attendanceLastSync", System.currentTimeMillis()); - r("finish", "Attendance"); - } - } - else { - if (attendanceLastSync != 0) { - // not a first sync - Date lastSyncDate = Date.fromMillis(attendanceLastSync); - lastSyncDate.stepForward(0, 0, 2); - if (lastSyncDate.getValue() >= attendanceCheckDate.getValue()) { - profile.putStudentData("attendanceLastSync", System.currentTimeMillis()); - } - } - r("finish", "Attendance"); - } - } - }) - .build() - .enqueue(); - } - - private void getAllMessages() { - if (!fullSync && onlyFeature != FEATURE_MESSAGES_OUTBOX) { - r("finish", "Messages"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_messages); - Request.builder() - .url(fakeLogin ? "https://szkolny.eu/mobimobi/mobi_mod_wiadomosci_szukaj.php" : "https://" + loginServerName + ".mobidziennik.pl/dziennik/wyszukiwarkawiadomosci?q=+") - .userAgent(System.getProperty("http.agent")) - .callback(new TextCallbackHandler() { - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 1012, CODE_OTHER, response, throwable)); - } - - @Override - public void onSuccess(String data, Response response) { - // just skip any failures here - if (data == null || data.equals("")) { - r("finish", "GradeDetails"); - return; - } - if (data.contains("nie-pamietam-hasla")) { - r("finish", "GradeDetails"); - return; - } - - try { - long startTime = System.currentTimeMillis(); - Document doc = Jsoup.parse(data); - - Element listElement = doc.getElementsByClass("spis").first(); - if (listElement == null) { - r("finish", "Messages"); - return; - } - Elements list = listElement.getElementsByClass("podswietl"); - for (Element item: list) { - long id = Long.parseLong(item.attr("rel").replaceAll("[^\\d]", "" )); - - Element subjectEl = item.select("td:eq(0) div").first(); - String subject = subjectEl.text(); - - Element addedDateEl = item.select("td:eq(1)").first(); - String addedDateStr = addedDateEl.text(); - long addedDate = Date.fromIsoHm(addedDateStr); - - Element typeEl = item.select("td:eq(2) img").first(); - int type = TYPE_RECEIVED; - if (typeEl.outerHtml().contains("mail_send.png")) - type = TYPE_SENT; - - Element senderEl = item.select("td:eq(3) div").first(); - long senderId = -1; - if (type == TYPE_RECEIVED) { - String senderName = senderEl.text(); - for (int i = 0; i < teachersMap.size(); i++) { - if (senderName.equals(teachersMap.valueAt(i))) { - senderId = teachersMap.keyAt(i); - break; - } - } - messageRecipientList.add(new MessageRecipient(profileId, -1, id)); - } - else { - // TYPE_SENT, so multiple recipients possible - String[] recipientNames = senderEl.text().split(", "); - for (String recipientName: recipientNames) { - long recipientId = -1; - for (int i = 0; i < teachersMap.size(); i++) { - if (recipientName.equals(teachersMap.valueAt(i))) { - recipientId = teachersMap.keyAt(i); - break; - } - } - messageRecipientIgnoreList.add(new MessageRecipient(profileId, recipientId, id)); - } - } - - Message message = new Message( - profileId, - id, - subject, - null, - type, - senderId, - -1 - ); - - messageList.add(message); - metadataList.add(new Metadata(profileId, Metadata.TYPE_MESSAGE, message.id, true, true, addedDate)); - } - - } catch (Exception e3) { - finishWithError(new AppError(TAG, 949, CODE_OTHER, response, e3, data)); - return; - } - - r("finish", "Messages"); - } - }) - .build() - .enqueue(); - } - - private void getMessagesInbox() { - callback.onActionStarted(R.string.sync_action_syncing_messages); - Request.builder() - .url(fakeLogin ? "https://szkolny.eu/mobimobi/mobi_mod_wiadomosci.php" : "https://" + loginServerName + ".mobidziennik.pl/dziennik/wiadomosci") - .userAgent(System.getProperty("http.agent")) - .callback(new TextCallbackHandler() { - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 972, CODE_OTHER, response, throwable)); - } - - @Override - public void onSuccess(String data, Response response) { - // just skip any failures here - if (data == null || data.equals("")) { - r("finish", "GradeDetails"); - return; - } - if (data.contains("nie-pamietam-hasla")) { - r("finish", "GradeDetails"); - return; - } - - try { - if (data.contains("Brak wiadomości odebranych.")) { - r("finish", "MessagesInbox"); - return; - } - Document doc = Jsoup.parse(data); - - Elements list = doc.getElementsByClass("spis").first().getElementsByClass("podswietl"); - for (Element item: list) { - long id = Long.parseLong(item.attr("rel")); - - Element subjectEl = item.select("td:eq(0)").first(); - //d(TAG, "subjectEl "+subjectEl.outerHtml()); - boolean hasAttachments = false; - if (subjectEl.getElementsByTag("a").size() != 0) { - hasAttachments = true; - } - String subject = subjectEl.ownText(); - - Element addedDateEl = item.select("td:eq(1) small").first(); - //d(TAG, "addedDateEl "+addedDateEl.outerHtml()); - String addedDateStr = addedDateEl.text(); - long addedDate = Date.fromIsoHm(addedDateStr); - - Element senderEl = item.select("td:eq(2)").first(); - //d(TAG, "senderEl "+senderEl.outerHtml()); - String senderName = senderEl.ownText(); - long senderId = -1; - for (int i = 0; i < teachersMap.size(); i++) { - if (senderName.equals(teachersMap.valueAt(i))) { - senderId = teachersMap.keyAt(i); - break; - } - } - messageRecipientIgnoreList.add(new MessageRecipient(profileId, -1, id)); - - boolean isRead = item.select("td:eq(3) span").first().hasClass("wiadomosc_przeczytana"); - - Message message = new Message( - profileId, - id, - subject, - null, - TYPE_RECEIVED, - senderId, - -1 - ); - - if (hasAttachments) - message.setHasAttachments(); - - - messageList.add(message); - messageMetadataList.add(new Metadata(profileId, Metadata.TYPE_MESSAGE, message.id, isRead, isRead || profile.getEmpty(), addedDate)); - } - r("finish", "MessagesInbox"); - } catch (Exception e3) { - finishWithError(new AppError(TAG, 1044, CODE_OTHER, response, e3, data)); - } - } - }) - .build() - .enqueue(); - } - - private SparseArray teachersMap = new SparseArray<>(); - private void processUsers(String table) - { - String[] users = table.split("\n"); - - //app.db.teacherDao().clear(profileId); - for (String userStr: users) - { - if (userStr.isEmpty()) { - continue; - } - String[] user = userStr.split("\\|", Integer.MAX_VALUE); - - teachersMap.put(strToInt(user[0]), user[5].trim()+" "+user[4].trim()); - teacherList.add(new Teacher(profileId, strToInt(user[0]), user[4].trim(), user[5].trim())); - } - } - - private void processDates(String table) - { - String[] dates = table.split("\n"); - for (String dateStr: dates) - { - if (dateStr.isEmpty()) { - continue; - } - String[] date = dateStr.split("\\|", Integer.MAX_VALUE); - switch (date[1]) { - case "semestr1_poczatek": - profile.setDateSemester1Start(Date.fromYmd(date[3])); - break; - case "semestr2_poczatek": - profile.setDateSemester2Start(Date.fromYmd(date[3])); - break; - case "koniec_roku_szkolnego": - profile.setDateYearEnd(Date.fromYmd(date[3])); - break; - } - } - } - - private SparseArray subjectsMap = new SparseArray<>(); - private void processSubjects(String table) - { - String[] subjects = table.split("\n"); - - // because a student may no longer have a subject - // which he had before - // therefore, this subject's grades do not get deleted - // so we should keep the subject for correct naming - //app.db.subjectDao().clear(profileId); - for (String subjectStr: subjects) - { - if (subjectStr.isEmpty()) { - continue; - } - String[] subject = subjectStr.split("\\|", Integer.MAX_VALUE); - - subjectsMap.put(strToInt(subject[0]), subject[1]); - subjectList.add(new Subject(profileId, strToInt(subject[0]), subject[1], subject[2])); - } - } - - private Team teamClass = null; - private SparseArray teamsMap = new SparseArray<>(); - private List allTeams = new ArrayList<>(); - private void processTeams(String tableTeams, String tableRelations) - { - if (tableTeams != null) - { - allTeams.clear(); - String[] teams = tableTeams.split("\n"); - for (String teamStr: teams) { - if (teamStr.isEmpty()) { - continue; - } - String[] team = teamStr.split("\\|", Integer.MAX_VALUE); - Team teamObject = new Team( - profileId, - strToInt(team[0]), - team[1]+team[2], - strToInt(team[3]), - loginServerName+":"+team[1]+team[2],// TODO zrobiłem to na szybko - strToInt(team[4], -1)); - allTeams.add(teamObject); - } - } - if (tableRelations != null) - { - app.db.teamDao().clear(profileId); - String[] teams = tableRelations.split("\n"); - for (String teamStr: teams) { - if (teamStr.isEmpty()) { - continue; - } - String[] team = teamStr.split("\\|", Integer.MAX_VALUE); - if (strToInt(team[1]) != studentId) - continue; - Team teamObject = Team.getById(allTeams, strToInt(team[2])); - if (teamObject != null) { - if (teamObject.type == 1) { - profile.setStudentNumber(strToInt(team[4])); - teamClass = teamObject; - } - teamsMap.put((int)teamObject.id, teamObject.name); - teamList.add(teamObject); - } - } - } - } - - private SparseArray> students = new SparseArray<>(); - private boolean processStudent(String table, String loginUsername) - { - students.clear(); - String[] student = table.split("\n"); - for (int i = 0; i < student.length; i++) { - if (student[i].isEmpty()) { - continue; - } - String[] student1 = student[i].split("\\|", Integer.MAX_VALUE); - String[] student2 = student[++i].split("\\|", Integer.MAX_VALUE); - students.put(strToInt(student1[0]), new Pair<>(student1, student2)); - } - Pair studentData = students.get(studentId); - try { - profile.setAttendancePercentage(Float.parseFloat(studentData.second[1])); - } - catch (Exception e) { - e.printStackTrace(); - } - return true; - } - - private SparseArray gradeCategoryList = new SparseArray<>(); - private void processGradeCategories(String table) - { - String[] gradeCategories = table.split("\n"); - - for (String gradeCategoryStr: gradeCategories) - { - if (gradeCategoryStr.isEmpty()) { - continue; - } - String[] gradeCategory = gradeCategoryStr.split("\\|", Integer.MAX_VALUE); - List columns = new ArrayList<>(); - Collections.addAll(columns, gradeCategory[7].split(";")); - - if (teamsMap.indexOfKey(strToInt(gradeCategory[1])) >= 0) { - gradeCategoryList.put( - Integer.parseInt(gradeCategory[0]), - new GradeCategory( - profileId, - Integer.parseInt(gradeCategory[0]), - Float.parseFloat(gradeCategory[3]), - Color.parseColor("#"+gradeCategory[6]), - gradeCategory[4] - ).addColumns(columns) - ); - } - } - } - - private class MobiLesson { - long id; - int subjectId; - int teacherId; - int teamId; - String topic; - Date date; - Time startTime; - Time endTime; - int presentCount; - int absentCount; - int lessonNumber; - String signed; - - MobiLesson(long id, int subjectId, int teacherId, int teamId, String topic, Date date, Time startTime, Time endTime, int presentCount, int absentCount, int lessonNumber, String signed) { - this.id = id; - this.subjectId = subjectId; - this.teacherId = teacherId; - this.teamId = teamId; - this.topic = topic; - this.date = date; - this.startTime = startTime; - this.endTime = endTime; - this.presentCount = presentCount; - this.absentCount = absentCount; - this.lessonNumber = lessonNumber; - this.signed = signed; - } - } - - private List mobiLessons = new ArrayList<>(); - - private MobiLesson getLesson(long id) { - for (MobiLesson mobiLesson : mobiLessons) { - if (mobiLesson.id == id) - return mobiLesson; - } - return null; - } - - private void processLessons(String table) - { - mobiLessons.clear(); - - String[] lessons2 = table.split("\n"); - for (String lessonStr: lessons2) - { - if (lessonStr.isEmpty()) { - continue; - } - String[] lesson = lessonStr.split("\\|", Integer.MAX_VALUE); - - MobiLesson newMobiLesson = new MobiLesson( - Long.parseLong(lesson[0]), - strToInt(lesson[1]), - strToInt(lesson[2]), - strToInt(lesson[3]), - lesson[4], - Date.fromYmd(lesson[5]), - Time.fromYmdHm(lesson[6]), - Time.fromYmdHm(lesson[7]), - strToInt(lesson[8]), - strToInt(lesson[9]), - strToInt(lesson[10]), - lesson[11]); - mobiLessons.add(newMobiLesson); - } - } - - private void processAttendance(String table) - { - if (true) - return; - String[] attendanceList = table.split("\n"); - for (String attendanceStr: attendanceList) - { - if (attendanceStr.isEmpty()) { - continue; - } - String[] attendance = attendanceStr.split("\\|", Integer.MAX_VALUE); - - if (strToInt(attendance[2]) != studentId) - continue; - //RegisterAttendance oldAttendance = RegisterAttendance.getById(oldAttendances, Long.parseLong(attendance[0])); - - MobiLesson mobiLesson = getLesson(Long.parseLong(attendance[1])); - if (mobiLesson != null) { - int type = TYPE_PRESENT; - switch (attendance[4]) { - case "2": - type = TYPE_ABSENT; - break; - case "5": - type = TYPE_ABSENT_EXCUSED; - break; - case "4": - type = TYPE_RELEASED; - break; - } - - int semester = profile.dateToSemester(mobiLesson.date); - - Attendance attendanceObject = new Attendance( - profileId, - strToInt(attendance[0]), - mobiLesson.teacherId, - mobiLesson.subjectId, - semester, - mobiLesson.topic, - mobiLesson.date, - mobiLesson.startTime, - type); - this.attendanceList.add(attendanceObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_ATTENDANCE, attendanceObject.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - } - - } - - private void processNotices(String table) - { - String[] notices = table.split("\n"); - - for (String noticeStr: notices) - { - if (noticeStr.isEmpty()) { - continue; - } - String[] notice = noticeStr.split("\\|", Integer.MAX_VALUE); - - if (strToInt(notice[2]) != studentId) - continue; - - Notice noticeObject = new Notice( - profileId, - strToInt(notice[0]), - notice[4], - strToInt(notice[6]), - (notice[3] != null ? (notice[3].equals("1") ? Notice.TYPE_POSITIVE : Notice.TYPE_NEGATIVE) : Notice.TYPE_NEUTRAL), - strToInt(notice[5]) - ); - - Metadata metadata = new Metadata(profileId, Metadata.TYPE_NOTICE, noticeObject.id, profile.getEmpty(), profile.getEmpty(), new Date().parseFromYmd(notice[7]).getInMillis()); - - noticeList.add(noticeObject); - metadataList.add(metadata); - } - } - - private void processGrades(String table) - { - app.db.gradeDao().getDetails(profileId, gradeAddedDates, gradeAverages, gradeColors); - - String[] grades = table.split("\n"); - - long addedDate = Date.getNowInMillis(); - for (String gradeStr: grades) - { - if (gradeStr.isEmpty()) { - continue; - } - String[] grade = gradeStr.split("\\|", Integer.MAX_VALUE); - - if (strToInt(grade[1]) != studentId) - continue; - - if (grade[6].equals("")) { - grade[6] = "-1"; - } - if (grade[10].equals("")) { - grade[10] = "1"; - } - - float weight = 0.0f; - String category = ""; - String description = ""; - int color = -1; - int categoryId = strToInt(grade[6]); - GradeCategory gradeCategory = gradeCategoryList.get(categoryId); - if (gradeCategory != null) { - weight = gradeCategory.weight; - category = gradeCategory.text; - description = gradeCategory.columns.get(strToInt(grade[10]) - 1); - color = gradeCategory.color; - } - - Grade gradeObject = new Grade( - profileId, - strToInt(grade[0]), - category, - color, - description, - grade[7], - Float.parseFloat(grade[11]), - weight, - strToInt(grade[5]), - strToInt(grade[2]), - strToInt(grade[3]) - ); - - - // fix for "0" value grades, so they're not counted in the average - if (gradeObject.value == 0.0f) { - gradeObject.weight = 0; - } - - switch (grade[8]) { - case "3": - // semester proposed - gradeObject.type = (gradeObject.semester == 1 ? TYPE_SEMESTER1_PROPOSED : TYPE_SEMESTER2_PROPOSED); - break; - case "1": - // semester final - gradeObject.type = (gradeObject.semester == 1 ? TYPE_SEMESTER1_FINAL : TYPE_SEMESTER2_FINAL); - break; - case "4": - // year proposed - gradeObject.type = TYPE_YEAR_PROPOSED; - break; - case "2": - // year final - gradeObject.type = TYPE_YEAR_FINAL; - break; - } - - Metadata metadata = new Metadata(profileId, Metadata.TYPE_GRADE, gradeObject.id, profile.getEmpty(), profile.getEmpty(), addedDate); - gradeList.add(gradeObject); - metadataList.add(metadata); - addedDate++; // increase the added date to sort grades as they are in the school profile - } - } - - private void processEvents(String table) - { - String[] events = table.split("\n"); - Date today = Date.getToday(); - for (String eventStr: events) - { - if (eventStr.isEmpty()) { - continue; - } - String[] event = eventStr.split("\\|", Integer.MAX_VALUE); - - Date eventDate = new Date().parseFromYmd(event[4]); - if (eventDate.getValue() < today.getValue()) - continue; - - String examType = ""; - Pattern pattern = Pattern.compile("\\(([0-9A-ząęóżźńśłć]*?)\\)$"); - Matcher matcher = pattern.matcher(event[5]); - if (matcher.find()) { - examType = matcher.group(1); - } - - long addedDate = 0; - - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss", Locale.US); - try { - addedDate = simpleDateFormat.parse(event[7]).getTime(); - } catch (ParseException e) { - e.printStackTrace(); - } - - int eventType = examType.equals("sprawdzian") ? TYPE_EXAM : examType.equals("kartkówka") ? TYPE_SHORT_QUIZ : TYPE_DEFAULT; - Event eventObject = new Event( - profileId, - strToInt(event[0]), - eventDate, - new Time().parseFromYmdHm(event[6]), - event[5].replace("("+examType+")", ""), - -1, - eventType, - false, - strToInt(event[1]), - strToInt(event[3]), - strToInt(event[2]) - ); - - if (teamsMap.indexOfKey((int)eventObject.teamId) >= 0) { - eventList.add(eventObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_EVENT, eventObject.id, profile.getEmpty(), profile.getEmpty(), addedDate)); - } - } - } - - private void processHomework(String table) - { - String[] homeworkList = table.split("\n"); - Date today = Date.getToday(); - for (String homeworkStr: homeworkList) - { - if (homeworkStr.isEmpty()) { - continue; - } - String[] homework = homeworkStr.split("\\|", Integer.MAX_VALUE); - - Date eventDate = new Date().parseFromYmd(homework[2]); - if (eventDate.getValue() < today.getValue()) - continue; - - Event eventObject = new Event( - profileId, - strToInt(homework[0]), - new Date().parseFromYmd(homework[2]), - new Time().parseFromYmdHm(homework[3]), - homework[1], - -1, - Event.TYPE_HOMEWORK, - false, - strToInt(homework[7]), - strToInt(homework[6]), - strToInt(homework[5]) - ); - - if (teamsMap.indexOfKey((int)eventObject.teamId) >= 0) { - eventList.add(eventObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_HOMEWORK, eventObject.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - } - } - - private void processTimetable(String table) - { - String[] lessons = table.split("\n"); - List list = Arrays.asList(lessons); - //Collections.reverse(list); - - // searching for all planned lessons - for (String lessonStr: (String[])list.toArray()) - { - //Log.d(TAG, lessonStr); - if (!lessonStr.isEmpty()) - { - String[] lesson = lessonStr.split("\\|", Integer.MAX_VALUE); - - if (strToInt(lesson[0]) != studentId) - continue; - - if (lesson[1].equals("plan_lekcji") || lesson[1].equals("lekcja")) - { - Lesson lessonObject = new Lesson(profileId, lesson[2], lesson[3], lesson[4]); - - for(int i = 0; i < subjectsMap.size(); i++) { - int key = subjectsMap.keyAt(i); - String str = subjectsMap.valueAt(i); - if (lesson[5].equalsIgnoreCase(str)) { - lessonObject.subjectId = key; - } - } - for(int i = 0; i < teachersMap.size(); i++) { - int key = teachersMap.keyAt(i); - String str = teachersMap.valueAt(i); - if ((lesson[7].trim() + " " + lesson[6].trim()).equalsIgnoreCase(str)) { - lessonObject.teacherId = key; - } - } - for(int i = 0; i < teamsMap.size(); i++) { - int key = teamsMap.keyAt(i); - String str = teamsMap.valueAt(i); - if ((lesson[8] + lesson[9]).equalsIgnoreCase(str)) { - lessonObject.teamId = key; - } - } - lessonObject.classroomName = lesson[11]; - lessonList.add(lessonObject); - } - } - } - - // searching for all changes - for (String lessonStr: (String[])list.toArray()) - { - if (!lessonStr.isEmpty()) - { - String[] lesson = lessonStr.split("\\|", Integer.MAX_VALUE); - - if (strToInt(lesson[0]) != studentId) - continue; - - if (lesson[1].equals("zastepstwo") || lesson[1].equals("lekcja_odwolana")) - { - LessonChange lessonChange = new LessonChange(profileId, lesson[2], lesson[3], lesson[4]); - - //Log.d(TAG, "Lekcja "+lessonStr); - - for(int i = 0; i < subjectsMap.size(); i++) { - int key = subjectsMap.keyAt(i); - String str = subjectsMap.valueAt(i); - if (lesson[5].equalsIgnoreCase(str)) { - lessonChange.subjectId = key; - } - } - for(int i = 0; i < teachersMap.size(); i++) { - int key = teachersMap.keyAt(i); - String str = teachersMap.valueAt(i); - if ((lesson[7].trim() + " " + lesson[6].trim()).equalsIgnoreCase(str)) { - lessonChange.teacherId = key; - } - } - for(int i = 0; i < teamsMap.size(); i++) { - int key = teamsMap.keyAt(i); - String str = teamsMap.valueAt(i); - if ((lesson[8] + lesson[9]).equalsIgnoreCase(str)) { - lessonChange.teamId = key; - } - } - - if (lesson[1].equals("zastepstwo")) { - lessonChange.type = LessonChange.TYPE_CHANGE; - } - if (lesson[1].equals("lekcja_odwolana")) { - lessonChange.type = LessonChange.TYPE_CANCELLED; - } - if (lesson[1].equals("lekcja")) { - lessonChange.type = LessonChange.TYPE_ADDED; - } - lessonChange.classroomName = lesson[11]; - - Lesson originalLesson = lessonChange.getOriginalLesson(lessonList); - - if (lessonChange.type == LessonChange.TYPE_ADDED) { - if (originalLesson == null) { - // original lesson doesn't exist, save a new addition - // TODO - /*if (!RegisterLessonChange.existsAddition(app.profile, registerLessonChange)) { - app.profile.timetable.addLessonAddition(registerLessonChange); - }*/ - } - else { - // original lesson exists, so we need to compare them - if (!lessonChange.matches(originalLesson)) { - // the lessons are different, so it's probably a lesson change - // ahhh this damn API - lessonChange.type = LessonChange.TYPE_CHANGE; - } - } - - } - if (lessonChange.type != LessonChange.TYPE_ADDED) { - // it's not a lesson addition - lessonChangeList.add(lessonChange); - metadataList.add(new Metadata(profileId, Metadata.TYPE_LESSON_CHANGE, lessonChange.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - if (originalLesson == null) { - // there is no original lesson, so we have to add one in order to change it - lessonList.add(Lesson.fromLessonChange(lessonChange)); - } - } - } - } - } - } - - @Override - public Map getConfigurableEndpoints(Profile profile) { - return null; - } - - @Override - public boolean isEndpointEnabled(Profile profile, boolean defaultActive, String name) { - return defaultActive; - } - - @Override - public void syncMessages(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile) { - - } - - /* __ __ - | \/ | - | \ / | ___ ___ ___ __ _ __ _ ___ ___ - | |\/| |/ _ \/ __/ __|/ _` |/ _` |/ _ \/ __| - | | | | __/\__ \__ \ (_| | (_| | __/\__ \ - |_| |_|\___||___/___/\__,_|\__, |\___||___/ - __/ | - |__*/ - @Override - public void getMessage(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull MessageFull message, @NonNull MessageGetCallback messageCallback) { - if (message.body != null) { - boolean readByAll = true; - message.recipients = app.db.messageRecipientDao().getAllByMessageId(profile.getId(), message.id); - for (MessageRecipientFull recipient: message.recipients) { - if (recipient.id == -1) - recipient.fullName = profile.getStudentNameLong(); - if (message.type == TYPE_SENT && recipient.readDate < 1) - readByAll = false; - } - if (!message.seen) { - app.db.metadataDao().setSeen(profile.getId(), message, true); - } - if (readByAll) { - // if a sent msg is not read by everyone, download it again to check the read status - new Handler(activityContext.getMainLooper()).post(() -> { - messageCallback.onSuccess(message); - }); - return; - } - } - - if (!prepare(activityContext, errorCallback, profile.getId(), profile, LoginStore.fromProfileFull(profile))) - return; - - login(() -> Request.builder() - .url("https://" + loginServerName + ".mobidziennik.pl/dziennik/"+(message.type == TYPE_RECEIVED || message.type == TYPE_DELETED ? "wiadodebrana" : "wiadwyslana")+"/?id="+message.id) - .userAgent(System.getProperty("http.agent")) - .callback(new TextCallbackHandler() { - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 1720, CODE_OTHER, response, throwable)); - } - - @Override - public void onSuccess(String data, Response response) { - if (data == null || data.equals("")) { - finishWithError(new AppError(TAG, 1727, CODE_MAINTENANCE, response)); - return; - } - if (data.contains("nie-pamietam-hasla")) { - lastLoginTime = 0; - finishWithError(new AppError(TAG, 1732, CODE_INVALID_LOGIN, response, data)); - return; - } - - List messageRecipientList = new ArrayList<>(); - - try { - Document doc = Jsoup.parse(data); - - Element content = doc.select("#content").first(); - - Element body = content.select(".wiadomosc_tresc").first(); - - if (message.type == TYPE_RECEIVED) { - MessageRecipientFull recipient = new MessageRecipientFull(profileId, -1, message.id); - long readDate = System.currentTimeMillis(); - Matcher matcher = Pattern.compile("czas przeczytania:.+?,\\s([0-9]+)\\s(.+?)\\s([0-9]{4}),\\sgodzina\\s([0-9:]+)", Pattern.DOTALL).matcher(body.html()); - if (matcher.find()) { - Date date = new Date( - strToInt(matcher.group(3)), - monthFromName(matcher.group(2)), - strToInt(matcher.group(1)) - ); - Time time = Time.fromH_m_s( - matcher.group(4) - ); - readDate = date.combineWith(time); - } - recipient.readDate = readDate; - recipient.fullName = profile.getStudentNameLong(); - messageRecipientList.add(recipient); - } - else { - message.senderId = -1; - message.senderReplyId = -1; - - List teacherList = app.db.teacherDao().getAllNow(profileId); - - Elements recipients = content.select("table.spis tr:has(td)"); - for (Element recipientEl: recipients) { - - Element senderEl = recipientEl.select("td:eq(0)").first(); - Teacher teacher = null; - String senderName = senderEl.text(); - for (Teacher aTeacher: teacherList) { - if (aTeacher.getFullNameLastFirst().equals(senderName)) { - teacher = aTeacher; - break; - } - } - - long readDate = 0; - Element isReadEl = recipientEl.select("td:eq(2)").first(); - if (!isReadEl.ownText().equals("NIE")) { - Element readDateEl = recipientEl.select("td:eq(3) small").first(); - Matcher matcher = Pattern.compile(".+?,\\s([0-9]+)\\s(.+?)\\s([0-9]{4}),\\sgodzina\\s([0-9:]+)", Pattern.DOTALL).matcher(readDateEl.ownText()); - if (matcher.find()) { - Date date = new Date( - strToInt(matcher.group(3)), - monthFromName(matcher.group(2)), - strToInt(matcher.group(1)) - ); - Time time = Time.fromH_m_s( - matcher.group(4) - ); - readDate = date.combineWith(time); - } - } - - MessageRecipientFull recipient = new MessageRecipientFull(profileId, teacher == null ? -1 : teacher.id, message.id); - recipient.readDate = readDate; - recipient.fullName = teacher == null ? "?" : teacher.getFullName(); - messageRecipientList.add(recipient); - } - } - - body.select("div").remove(); - - message.body = body.html(); - - message.clearAttachments(); - Elements attachments = content.select("ul li"); - if (attachments != null) { - //d(TAG, "attachments "+attachments.outerHtml()); - for (Element attachment: attachments) { - attachment = attachment.select("a").first(); - //d(TAG, "attachment "+attachment.outerHtml()); - String attachmentName = attachment.ownText(); - long attachmentId = -1; - Matcher matcher = Pattern.compile("href=\"https://.+?\\.mobidziennik.pl/.+?&(?:amp;)?zalacznik=([0-9]+)\"(?:.+?)(List) messageRecipientList); - - message.recipients = messageRecipientList; - - new Handler(activityContext.getMainLooper()).post(() -> { - messageCallback.onSuccess(message); - }); - } - }) - .build() - .enqueue()); - } - - @Override - public void getAttachment(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull MessageFull message, long attachmentId, @NonNull AttachmentGetCallback attachmentCallback) { - if (!prepare(activityContext, errorCallback, profile.getId(), profile, LoginStore.fromProfileFull(profile))) - return; - - login(() -> { - Request.Builder builder = Request.builder() - .url("https://"+loginServerName+".mobidziennik.pl/dziennik/"+(message.type == TYPE_SENT ? "wiadwyslana" : "wiadodebrana")+"/?id="+message.id+"&zalacznik="+attachmentId); - new Handler(activityContext.getMainLooper()).post(() -> { - attachmentCallback.onSuccess(builder); - }); - }); - } - - @Override - public void getRecipientList(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull RecipientListGetCallback recipientListGetCallback) { - if (!prepare(activityContext, errorCallback, profile.getId(), profile, LoginStore.fromProfileFull(profile))) - return; - - if (System.currentTimeMillis() - profile.getLastReceiversSync() < 24 * 60 * 60 * 1000) { - AsyncTask.execute(() -> { - List teacherList = app.db.teacherDao().getAllNow(profileId); - new Handler(activityContext.getMainLooper()).post(() -> recipientListGetCallback.onSuccess(teacherList)); - }); - return; - } - - login(() -> { - Request.builder() - .url("https://" + loginServerName + ".mobidziennik.pl/mobile/dodajwiadomosc") - .userAgent(System.getProperty("http.agent")) - .callback(new TextCallbackHandler() { - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 2289, CODE_OTHER, response, throwable)); - } - - @Override - public void onSuccess(String data, Response response) { - if (data == null || data.equals("")) { - finishWithError(new AppError(TAG, 2295, CODE_MAINTENANCE, response)); - return; - } - if (data.contains("nie-pamietam-hasla")) { - lastLoginTime = 0; - finishWithError(new AppError(TAG, 2300, CODE_INVALID_LOGIN, response, data)); - return; - } - - teacherList = app.db.teacherDao().getAllNow(profileId); - - for (Teacher teacher: teacherList) { - teacher.typeDescription = null; // TODO: 2019-06-13 it better - } - - Matcher categoryMatcher = Pattern.compile("").matcher(data); - while (categoryMatcher.find()) { - String categoryId = categoryMatcher.group(1); - String categoryName = categoryMatcher.group(2); - String categoryHtml = getRecipientCategory(categoryId); - if (categoryHtml == null) - return; // the error callback is already executed - Matcher teacherMatcher = Pattern.compile("(.+?)|").matcher(categoryHtml); - while (teacherMatcher.find()) { - if (teacherMatcher.group(1) != null) { - String className = teacherMatcher.group(1); - String listHtml = teacherMatcher.group(2); - Matcher listMatcher = Pattern.compile("").matcher(listHtml); - while (listMatcher.find()) { - updateTeacher(categoryId, Long.parseLong(listMatcher.group(1)), listMatcher.group(2), categoryName, className); - } - } - else { - updateTeacher(categoryId, Long.parseLong(teacherMatcher.group(3)), teacherMatcher.group(4), categoryName, null); - } - } - } - app.db.teacherDao().addAll(teacherList); - - profile.setLastReceiversSync(System.currentTimeMillis()); - app.db.profileDao().add(profile); - - new Handler(activityContext.getMainLooper()).post(() -> recipientListGetCallback.onSuccess(new ArrayList<>(teacherList))); - } - }) - .build() - .enqueue(); - }); - } - private String getRecipientCategory(String categoryId) { - Response response = null; - try { - response = Request.builder() - .url("https://" + loginServerName + ".mobidziennik.pl/mobile/odbiorcyWiadomosci") - .userAgent(System.getProperty("http.agent")) - .addParameter("typ", categoryId) - .post() - .build().execute(); - if (response.code() != 200) { - finishWithError(new AppError(TAG, 2349, CODE_OTHER, response)); - return null; - } - String data = new TextCallbackHandler().backgroundParser(response); - if (data == null || data.equals("")) { - return ""; - } - if (data.contains("nie-pamietam-hasla")) { - lastLoginTime = 0; - finishWithError(new AppError(TAG, 2300, CODE_INVALID_LOGIN, response, data)); - return null; - } - return data; - } catch (Exception e) { - finishWithError(new AppError(TAG, 2355, CODE_OTHER, response, e)); - return null; - } - } - private void updateTeacher(String category, long id, String nameLastFirst, String typeDescription, String className) { - Teacher teacher = Teacher.getById(teacherList, id); - if (teacher == null) { - String[] nameParts = nameLastFirst.split(" ", Integer.MAX_VALUE); - teacher = new Teacher(profileId, id, nameParts[1], nameParts[0]); - teacherList.add(teacher); - } - teacher.loginId = String.valueOf(id); - teacher.type = 0; - if (className != null) { - teacher.typeDescription = bs("", teacher.typeDescription, ", ")+className; - } - switch (category) { - case "1": - teacher.setType(Teacher.TYPE_PRINCIPAL); - break; - case "2": - teacher.setType(Teacher.TYPE_TEACHER); - break; - case "8": - teacher.setType(Teacher.TYPE_EDUCATOR); - break; - case "9": - teacher.setType(Teacher.TYPE_PEDAGOGUE); - break; - case "10": - teacher.setType(Teacher.TYPE_OTHER); - teacher.typeDescription = "Specjaliści"; - break; - case "Administracja WizjaNet": - teacher.setType(Teacher.TYPE_SUPER_ADMIN); - break; - case "Administratorzy": - teacher.setType(Teacher.TYPE_SCHOOL_ADMIN); - break; - case "Sekretarka": - teacher.setType(Teacher.TYPE_SECRETARIAT); - break; - default: - teacher.setType(Teacher.TYPE_OTHER); - teacher.typeDescription = typeDescription+bs(" ", teacher.typeDescription); - break; - } - } - - @Override - public MessagesComposeInfo getComposeInfo(@NonNull ProfileFull profile) { - return new MessagesComposeInfo(-1, profile.getStudentData("attachmentSizeLimit", 6291456L), 100, -1); - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt new file mode 100644 index 00000000..2de3cca5 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt @@ -0,0 +1,171 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-6. + */ + +package pl.szczodrzynski.edziennik.data.api + +import kotlin.text.RegexOption.DOT_MATCHES_ALL + +object Regexes { + val STYLE_CSS_COLOR by lazy { + """color: \w+?;?"?""".toRegex() + } + + + + val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy { + """\n*\s*(.+?)\s*\n*(?:<.*?)??
""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_GRADES_COLOR by lazy { + """background-color:([#A-Fa-f0-9]+);""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_GRADES_CATEGORY by lazy { + """> (.+?):
""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_GRADES_CLASS_AVERAGE by lazy { + """Średnia ocen:.*([0-9]*\.?[0-9]*)""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_GRADES_ADDED_DATE by lazy { + """Wpisano:.*.+?,\s([0-9]+)\s(.+?)\s([0-9]{4}),\sgodzina\s([0-9:]+)""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_GRADES_COUNT_TO_AVG by lazy { + """Liczona do średniej:.*?nie
""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_GRADES_DETAILS by lazy { + """(.+?).*?.+?.*?(?:\((.+?)\).*?)?.*?Wartość oceny:.*?([0-9.]+).*?Wpisał\(a\):.*?(.+?).*?(?:Komentarz:.*?(.+?))?""".toRegex(DOT_MATCHES_ALL) + } + + val MOBIDZIENNIK_EVENT_TYPE by lazy { + """\(([0-9A-ząęóżźńśłć]*?)\)$""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_LUCKY_NUMBER by lazy { + """class="szczesliwy_numerek".*>0*([0-9]+)(?:/0*[0-9]+)*""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_CLASS_CALENDAR by lazy { + """events: (.+),$""".toRegex(RegexOption.MULTILINE) + } + + val MOBIDZIENNIK_MESSAGE_READ_DATE by lazy { + """czas przeczytania:.+?,\s([0-9]+)\s(.+?)\s([0-9]{4}),\sgodzina\s([0-9:]+)""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_MESSAGE_SENT_READ_DATE by lazy { + """.+?,\s([0-9]+)\s(.+?)\s([0-9]{4}),\sgodzina\s([0-9:]+)""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_MESSAGE_ATTACHMENT by lazy { + """href="https://.+?\.mobidziennik.pl/.+?&(?:amp;)?zalacznik=([0-9]+)"(?:.+?""".toRegex(DOT_MATCHES_ALL) + } + val IDZIENNIK_LOGIN_ERROR by lazy { + """id="spanErrorMessage">(.*?)(.+?)""".toRegex(DOT_MATCHES_ALL) + } + val IDZIENNIK_LOGIN_FIRST_IS_PARENT by lazy { + """id="ctl00_CzyRodzic" value="([01])" />""".toRegex() + } + val IDZIENNIK_LOGIN_FIRST_SCHOOL_YEAR by lazy { + """name="ctl00\${"$"}dxComboRokSzkolny".+?selected="selected".*?value="([0-9]+)">([0-9/]+)<""".toRegex(DOT_MATCHES_ALL) + } + val IDZIENNIK_LOGIN_FIRST_STUDENT_SELECT by lazy { + """""".toRegex(DOT_MATCHES_ALL) + } + val IDZIENNIK_LOGIN_FIRST_STUDENT by lazy { + """(.+?)\s(.+?)\s*\((.+?),\s*(.+?)\)""".toRegex(DOT_MATCHES_ALL) + } + val IDZIENNIK_MESSAGES_RECIPIENT_PARENT by lazy { + """(.+?)\s\((.+)\)""".toRegex() + } + + + + val VULCAN_SHIFT_ANNOTATION by lazy { + """\(przeniesiona (z|na) lekcj[ię] ([0-9]+), (.+)\)""".toRegex() + } + + + + val LIBRUS_ATTACHMENT_KEY by lazy { + """singleUseKey=([0-9A-f_]+)""".toRegex() + } + + + + val EDUDZIENNIK_STUDENTS_START by lazy { + """
  • (.*?)""".toRegex() + } + val EDUDZIENNIK_ACCOUNT_NAME_START by lazy { + """(.*?)""".toRegex() + } + val EDUDZIENNIK_SUBJECTS_START by lazy { + """(.+?)""".toRegex() + } + + val EDUDZIENNIK_ATTENDANCE_ENTRIES by lazy { + """(.+?)""".toRegex() + } + val EDUDZIENNIK_ATTENDANCE_TYPES by lazy { + """
    .*?

    (.*?)

    """.toRegex(DOT_MATCHES_ALL) + } + val EDUDZIENNIK_ATTENDANCE_TYPE by lazy { + """\((.+?)\) (.+)""".toRegex() + } + + val EDUDZIENNIK_ANNOUNCEMENT_DESCRIPTION by lazy { + """
    .*?

    (.*?)

    """.toRegex(DOT_MATCHES_ALL) + } + + val EDUDZIENNIK_SUBJECT_ID by lazy { + """/Courses/([\w-_]+?)/""".toRegex() + } + val EDUDZIENNIK_GRADE_ID by lazy { + """/Grades/([\w-_]+?)/""".toRegex() + } + val EDUDZIENNIK_EXAM_ID by lazy { + """/Evaluations/([\w-_]+?)/""".toRegex() + } + val EDUDZIENNIK_EVENT_TYPE_ID by lazy { + """/GradeLabels/([\w-_]+?)/""".toRegex() + } + val EDUDZIENNIK_ANNOUNCEMENT_ID by lazy { + """/Announcement/([\w-_]+?)/""".toRegex() + } + val EDUDZIENNIK_HOMEWORK_ID by lazy { + """/Homework/([\w-_]+?)/""".toRegex() + } + val EDUDZIENNIK_TEACHER_ID by lazy { + """/Teachers/([\w-_]+?)/""".toRegex() + } + val EDUDZIENNIK_EVENT_ID by lazy { + """/KlassEvent/([\w-_]+?)/""".toRegex() + } + val EDUDZIENNIK_NOTE_ID by lazy { + """/RegistryNotes/([\w-_]+?)/""".toRegex() + } + + val EDUDZIENNIK_SCHOOL_DETAIL_ID by lazy { + """.*?

    (.*?)

    .*?
  • """.toRegex(DOT_MATCHES_ALL) + } + val EDUDZIENNIK_CLASS_DETAIL_ID by lazy { + """(.*?)""".toRegex(DOT_MATCHES_ALL) + } + + val EDUDZIENNIK_TEACHERS by lazy { + """
    .*?

    (.+?) (.+?)

    """.toRegex(DOT_MATCHES_ALL) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Vulcan.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Vulcan.java deleted file mode 100644 index 090c9393..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Vulcan.java +++ /dev/null @@ -1,1688 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api; - -import android.content.Context; -import android.os.Build; -import android.os.Handler; -import android.util.Base64; -import android.util.Pair; -import android.util.SparseArray; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.crashlytics.android.Crashlytics; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonNull; -import com.google.gson.JsonObject; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import im.wangchao.mhttp.Request; -import im.wangchao.mhttp.Response; -import im.wangchao.mhttp.callback.JsonCallbackHandler; -import okhttp3.OkHttpClient; -import okio.Buffer; -import pl.szczodrzynski.edziennik.App; -import pl.szczodrzynski.edziennik.R; -import pl.szczodrzynski.edziennik.data.api.interfaces.AttachmentGetCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface; -import pl.szczodrzynski.edziennik.data.api.interfaces.LoginCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.MessageGetCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.RecipientListGetCallback; -import pl.szczodrzynski.edziennik.data.api.interfaces.SyncCallback; -import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance; -import pl.szczodrzynski.edziennik.data.db.modules.events.Event; -import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade; -import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory; -import pl.szczodrzynski.edziennik.data.db.modules.lessons.Lesson; -import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange; -import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore; -import pl.szczodrzynski.edziennik.data.db.modules.messages.Message; -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull; -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient; -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipientFull; -import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata; -import pl.szczodrzynski.edziennik.data.db.modules.notices.Notice; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull; -import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject; -import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher; -import pl.szczodrzynski.edziennik.data.db.modules.teams.Team; -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesComposeInfo; -import pl.szczodrzynski.edziennik.utils.models.Date; -import pl.szczodrzynski.edziennik.utils.models.Endpoint; -import pl.szczodrzynski.edziennik.utils.models.Time; -import pl.szczodrzynski.edziennik.utils.models.Week; -import pl.szczodrzynski.edziennik.utils.Utils; - -import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_OTHER; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_ABSENT; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_ABSENT_EXCUSED; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_BELATED; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_BELATED_EXCUSED; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_PRESENT; -import static pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_RELEASED; -import static pl.szczodrzynski.edziennik.data.db.modules.events.Event.TYPE_EXAM; -import static pl.szczodrzynski.edziennik.data.db.modules.events.Event.TYPE_SHORT_QUIZ; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER1_FINAL; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER1_PROPOSED; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER2_FINAL; -import static pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER2_PROPOSED; -import static pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange.TYPE_CANCELLED; -import static pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange.TYPE_CHANGE; -import static pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_VULCAN; -import static pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED; -import static pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT; -import static pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata.TYPE_MESSAGE; -import static pl.szczodrzynski.edziennik.data.db.modules.notices.Notice.TYPE_NEUTRAL; -import static pl.szczodrzynski.edziennik.utils.Utils.c; -import static pl.szczodrzynski.edziennik.utils.Utils.crc16; -import static pl.szczodrzynski.edziennik.utils.Utils.d; -import static pl.szczodrzynski.edziennik.utils.Utils.getGradeValue; -import static pl.szczodrzynski.edziennik.utils.Utils.getVulcanGradeColor; -import static pl.szczodrzynski.edziennik.utils.Utils.intToStr; - -public class Vulcan implements EdziennikInterface { - public Vulcan(App app) { - this.app = app; - } - - private static final String TAG = "api.Vulcan"; - private static final String MOBILE_APP_NAME = "VULCAN-Android-ModulUcznia"; - private static final String MOBILE_APP_VERSION = "19.4.1.436"; - private static final String ENDPOINT_CERTIFICATE = "mobile-api/Uczen.v3.UczenStart/Certyfikat"; - private static final String ENDPOINT_STUDENT_LIST = "mobile-api/Uczen.v3.UczenStart/ListaUczniow"; - private static final String ENDPOINT_DICTIONARIES = "mobile-api/Uczen.v3.Uczen/Slowniki"; - private static final String ENDPOINT_TIMETABLE = "mobile-api/Uczen.v3.Uczen/PlanLekcjiZeZmianami"; - private static final String ENDPOINT_GRADES = "mobile-api/Uczen.v3.Uczen/Oceny"; - private static final String ENDPOINT_GRADES_PROPOSITIONS = "mobile-api/Uczen.v3.Uczen/OcenyPodsumowanie"; - private static final String ENDPOINT_EVENTS = "mobile-api/Uczen.v3.Uczen/Sprawdziany"; - private static final String ENDPOINT_HOMEWORK = "mobile-api/Uczen.v3.Uczen/ZadaniaDomowe"; - private static final String ENDPOINT_NOTICES = "mobile-api/Uczen.v3.Uczen/UwagiUcznia"; - private static final String ENDPOINT_ATTENDANCE = "mobile-api/Uczen.v3.Uczen/Frekwencje"; - private static final String ENDPOINT_MESSAGES_RECEIVED = "mobile-api/Uczen.v3.Uczen/WiadomosciOdebrane"; - private static final String ENDPOINT_MESSAGES_SENT = "mobile-api/Uczen.v3.Uczen/WiadomosciWyslane"; - private static final String ENDPOINT_MESSAGES_CHANGE_STATUS = "mobile-api/Uczen.v3.Uczen/ZmienStatusWiadomosci"; - private static final String ENDPOINT_PUSH = "mobile-api/Uczen.v3.Uczen/UstawPushToken"; - private static final String userAgent = "MobileUserAgent"; - - private App app; - private Context activityContext = null; - private SyncCallback callback = null; - private int profileId = -1; - private Profile profile = null; - private LoginStore loginStore = null; - private boolean fullSync = true; - private Date today = Date.getToday(); - private List targetEndpoints = new ArrayList<>(); - - // PROGRESS - private static final int PROGRESS_LOGIN = 10; - private int PROGRESS_COUNT = 1; - private int PROGRESS_STEP = (90/PROGRESS_COUNT); - - private int onlyFeature = FEATURE_ALL; - - private List teamList; - private List teacherList; - private List subjectList; - private List lessonList; - private List lessonChangeList; - private List gradeCategoryList; - private List gradeList; - private List eventList; - private List noticeList; - private List attendanceList; - private List messageList; - private List messageRecipientList; - private List messageRecipientIgnoreList; - private List metadataList; - private List messageMetadataList; - - private String apiUrl = null; - private String certificateKey = null; - private String certificatePfx = null; - private String deviceToken = null; - private String deviceSymbol = null; - private String devicePin = null; - /** - * deviceSymbol_JednostkaSprawozdawczaSymbol - */private String schoolName = null; - /** - * JednostkaSprawozdawczaSymbol - */private String schoolSymbol = null; - /** - * Id - */private int studentId = -1; - /** - * UzytkownikLoginId - */private int studentLoginId = -1; - /** - * IdOddzial - */private int studentClassId = -1; - /** - * IdOkresKlasyfikacyjny - */private int studentSemesterId = -1; - /** - * OkresNumer - */private int studentSemesterNumber = -1; - private boolean syncingSemester1 = false; - private OkHttpClient signingHttp = null; - private Date oneMonthBack = today.clone().stepForward(0, -1, 0); - - /* _____ - | __ \ - | |__) | __ ___ _ __ __ _ _ __ ___ - | ___/ '__/ _ \ '_ \ / _` | '__/ _ \ - | | | | | __/ |_) | (_| | | | __/ - |_| |_| \___| .__/ \__,_|_| \___| - | | - |*/ - private boolean prepare(@NonNull Context activityContext, @NonNull SyncCallback callback, int profileId, @Nullable Profile profile, @NonNull LoginStore loginStore) { - this.activityContext = activityContext; - this.callback = callback; - this.profileId = profileId; - // here we must have a login store: either with a correct ID or -1 - // there may be no profile and that's when onLoginFirst happens - this.profile = profile; - this.loginStore = loginStore; - this.fullSync = profile == null || profile.getEmpty() || profile.shouldFullSync(activityContext); - this.today = Date.getToday(); - - if (this.signingHttp == null) { - this.signingHttp = this.app.http.newBuilder().addInterceptor(chain -> { - okhttp3.Request request = chain.request(); - Buffer buffer = new Buffer(); - assert request.body() != null; - request.body().writeTo(buffer); - String signature = ""; - // cannot use Utils.exception, because we are not in the main thread here! - // Utils.exception would show the dialog which has to be shown in the UI thread - try { - signature = Utils.VulcanRequestEncryptionUtils.signContent(buffer.readByteArray(), new ByteArrayInputStream(Base64.decode(this.certificatePfx, Base64.DEFAULT))); - } catch (IOException e) { - e.printStackTrace(); - } catch (GeneralSecurityException e) { - e.printStackTrace(); - } - request = request.newBuilder().addHeader("RequestSignatureValue", signature).addHeader("RequestCertificateKey", this.certificateKey).build(); - return chain.proceed(request); - }).build(); - } - - - if (certificateKey == null || certificatePfx == null || apiUrl == null) { - c(TAG, "first login, use TOKEN, SYMBOL, PIN"); - - } - - teamList = profileId == -1 ? new ArrayList<>() : app.db.teamDao().getAllNow(profileId); - teacherList = profileId == -1 ? new ArrayList<>() : app.db.teacherDao().getAllNow(profileId); - subjectList = new ArrayList<>(); - lessonList = new ArrayList<>(); - lessonChangeList = new ArrayList<>(); - gradeCategoryList = new ArrayList<>(); - gradeList = new ArrayList<>(); - eventList = new ArrayList<>(); - noticeList = new ArrayList<>(); - attendanceList = new ArrayList<>(); - messageList = new ArrayList<>(); - messageRecipientList = new ArrayList<>(); - messageRecipientIgnoreList = new ArrayList<>(); - metadataList = new ArrayList<>(); - messageMetadataList = new ArrayList<>(); - - return true; - } - - @Override - public void sync(@NonNull Context activityContext, @NonNull SyncCallback callback, int profileId, @Nullable Profile profile, @NonNull LoginStore loginStore) { - if (!prepare(activityContext, callback, profileId, profile, loginStore)) - return; - - login(() -> { - targetEndpoints = new ArrayList<>(); - targetEndpoints.add("SetPushToken"); - targetEndpoints.add("Dictionaries"); - targetEndpoints.add("Timetable"); - targetEndpoints.add("Grades"); - targetEndpoints.add("ProposedGrades"); - targetEndpoints.add("Events"); - targetEndpoints.add("Homework"); - targetEndpoints.add("Notices"); - targetEndpoints.add("Attendance"); - targetEndpoints.add("MessagesInbox"); - targetEndpoints.add("MessagesOutbox"); - targetEndpoints.add("Finish"); - PROGRESS_COUNT = targetEndpoints.size()-1; - PROGRESS_STEP = (90/PROGRESS_COUNT); - begin(); - }); - } - @Override - public void syncFeature(@NonNull Context activityContext, @NonNull SyncCallback callback, @NonNull ProfileFull profile, int ... featureList) { - if (featureList == null) { - sync(activityContext, callback, profile.getId(), profile, LoginStore.fromProfileFull(profile)); - return; - } - if (!prepare(activityContext, callback, profile.getId(), profile, LoginStore.fromProfileFull(profile))) - return; - - login(() -> { - targetEndpoints = new ArrayList<>(); - if (featureList.length == 1) - onlyFeature = featureList[0]; - targetEndpoints.add("SetPushToken"); - targetEndpoints.add("Dictionaries"); - for (int feature: featureList) { - switch (feature) { - case FEATURE_TIMETABLE: - targetEndpoints.add("Timetable"); - break; - case FEATURE_AGENDA: - targetEndpoints.add("Events"); - break; - case FEATURE_GRADES: - targetEndpoints.add("Grades"); - targetEndpoints.add("ProposedGrades"); - break; - case FEATURE_HOMEWORK: - targetEndpoints.add("Homework"); - break; - case FEATURE_NOTICES: - targetEndpoints.add("Notices"); - break; - case FEATURE_ATTENDANCE: - targetEndpoints.add("Attendance"); - break; - case FEATURE_MESSAGES_INBOX: - targetEndpoints.add("MessagesInbox"); - break; - case FEATURE_MESSAGES_OUTBOX: - targetEndpoints.add("MessagesOutbox"); - break; - } - } - targetEndpoints.add("Finish"); - PROGRESS_COUNT = targetEndpoints.size()-1; - PROGRESS_STEP = (90/PROGRESS_COUNT); - begin(); - }); - } - - private void begin() { - schoolName = profile.getStudentData("schoolName", null); - schoolSymbol = profile.getStudentData("schoolSymbol", null); - studentId = profile.getStudentData("studentId", -1); - studentLoginId = profile.getStudentData("studentLoginId", -1); - studentClassId = profile.getStudentData("studentClassId", -1); - studentSemesterId = profile.getStudentData("studentSemesterId", -1); - studentSemesterNumber = profile.getStudentData("studentSemesterNumber", profile.getCurrentSemester()); - if (profile.getEmpty() && studentSemesterNumber == 2) { - syncingSemester1 = true; - studentSemesterId -= 1; - studentSemesterNumber -= 1; - } - else { - syncingSemester1 = false; - } - d(TAG, "Syncing student "+studentId+", class "+studentClassId+", semester "+studentSemesterId); - - callback.onProgress(PROGRESS_LOGIN); - - r("get", null); - } - - private void r(String type, String endpoint) { - // endpoint == null when beginning - if (endpoint == null) - endpoint = targetEndpoints.get(0); - int index = -1; - for (String request: targetEndpoints) { - index++; - if (request.equals(endpoint)) { - break; - } - } - if (type.equals("finish")) { - // called when finishing the action - callback.onProgress(PROGRESS_STEP); - index++; - } - d(TAG, "Called r("+type+", "+endpoint+"). Getting "+targetEndpoints.get(index)); - switch (targetEndpoints.get(index)) { - case "SetPushToken": - setPushToken(); - break; - case "Dictionaries": - getDictionaries(); - break; - case "Timetable": - getTimetable(); - break; - case "Grades": - getGrades(); - break; - case "ProposedGrades": - getProposedGrades(); - break; - case "Events": - getEvents(); - break; - case "Homework": - getHomework(); - break; - case "Notices": - getNotices(); - break; - case "Attendance": - getAttendance(); - break; - case "MessagesInbox": - getMessagesInbox(); - break; - case "MessagesOutbox": - getMessagesOutbox(); - break; - case "Finish": - finish(); - break; - } - } - private void saveData() { - if (teamList.size() > 0) { - app.db.teamDao().addAll(teamList); - } - if (teacherList.size() > 0) - app.db.teacherDao().addAll(teacherList); - if (subjectList.size() > 0) - app.db.subjectDao().addAll(subjectList); - if (lessonList.size() > 0) { - app.db.lessonDao().clear(profileId); - app.db.lessonDao().addAll(lessonList); - } - if (lessonChangeList.size() > 0) - app.db.lessonChangeDao().addAll(lessonChangeList); - if (gradeCategoryList.size() > 0) - app.db.gradeCategoryDao().addAll(gradeCategoryList); - if (gradeList.size() > 0) { - app.db.gradeDao().clearForSemester(profileId, studentSemesterNumber); - app.db.gradeDao().addAll(gradeList); - } - if (eventList.size() > 0) { - app.db.eventDao().removeFuture(profileId, Date.getToday()); - app.db.eventDao().addAll(eventList); - } - if (noticeList.size() > 0) { - app.db.noticeDao().clearForSemester(profileId, studentSemesterNumber); - app.db.noticeDao().addAll(noticeList); - } - if (attendanceList.size() > 0) { - app.db.attendanceDao().clearAfterDate(profileId, getCurrentSemesterStartDate()); - app.db.attendanceDao().addAll(attendanceList); - } - if (messageList.size() > 0) - app.db.messageDao().addAllIgnore(messageList); - if (messageRecipientList.size() > 0) - app.db.messageRecipientDao().addAll(messageRecipientList); - if (messageRecipientIgnoreList.size() > 0) - app.db.messageRecipientDao().addAllIgnore(messageRecipientIgnoreList); - if (metadataList.size() > 0) - app.db.metadataDao().addAllIgnore(metadataList); - if (messageMetadataList.size() > 0) - app.db.metadataDao().setSeen(messageMetadataList); - } - private void finish() { - if (syncingSemester1) { - syncingSemester1 = false; - studentSemesterId += 1; - studentSemesterNumber += 1; - // no need to download dictionaries again - r("get", null);// TODO: 2019-06-04 start with Timetables or first element (if partial sync) - return; - } - try { - saveData(); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 425, CODE_OTHER, app.getString(R.string.sync_error_saving_data), null, null, e, null)); - } - if (fullSync) { - profile.setLastFullSync(System.currentTimeMillis()); - fullSync = false; - } - profile.setEmpty(false); - callback.onSuccess(activityContext, new ProfileFull(profile, loginStore)); - } - private void finishWithError(AppError error) { - try { - saveData(); - } - catch (Exception e) { - Crashlytics.logException(e); - } - callback.onError(activityContext, error); - } - - /* _ _ - | | (_) - | | ___ __ _ _ _ __ - | | / _ \ / _` | | '_ \ - | |___| (_) | (_| | | | | | - |______\___/ \__, |_|_| |_| - __/ | - |__*/ - private void login(@NonNull LoginCallback loginCallback) { - if (profile == null) { - // FIRST LOGIN - this.deviceToken = loginStore.getLoginData("deviceToken", null); - this.deviceSymbol = loginStore.getLoginData("deviceSymbol", null); - this.devicePin = loginStore.getLoginData("devicePin", null); - if (deviceToken == null || deviceSymbol == null || devicePin == null) { - finishWithError(new AppError(TAG, 443, AppError.CODE_INVALID_LOGIN, "Login field is empty")); - return; - } - - setApiUrl(deviceToken, deviceSymbol); - getCertificate(deviceToken, devicePin, ((certificateKey, certificatePfx, apiUrl, userLogin, userName) -> { - loginStore.removeLoginData("deviceToken"); - loginStore.removeLoginData("devicePin"); - loginStore.putLoginData("certificateKey", certificateKey); - loginStore.putLoginData("certificatePfx", certificatePfx); - loginStore.putLoginData("apiUrl", apiUrl); - loginStore.putLoginData("userLogin", userLogin); - this.apiUrl = apiUrl; - this.certificateKey = certificateKey; - this.certificatePfx = certificatePfx; - this.deviceToken = null; - this.devicePin = null; - - c(TAG, "first login. get the list of students"); - getStudentList(usersMap -> { - List profileList = new ArrayList<>(); - for (int index = 0; index < usersMap.size(); index++) { - JsonObject account = new ArrayList<>(usersMap.values()).get(index); - - Profile newProfile = new Profile(); - newProfile.setEmpty(true); - newProfile.setLoggedIn(true); - saveStudentData(newProfile, account); - - profileList.add(newProfile); - } - callback.onLoginFirst(profileList, loginStore); - }, false); - - })); - } - else { - this.apiUrl = loginStore.getLoginData("apiUrl", null); - this.certificateKey = loginStore.getLoginData("certificateKey", null); - this.certificatePfx = loginStore.getLoginData("certificatePfx", null); - if (apiUrl == null || certificateKey == null || certificatePfx == null) { - finishWithError(new AppError(TAG, 489, AppError.CODE_INVALID_LOGIN, "Login field is empty")); - return; - } - if (profile.getStudentData("currentSemesterEndDate", System.currentTimeMillis()/*default always larger*/) < System.currentTimeMillis()/1000 - || (studentLoginId = profile.getStudentData("studentLoginId", -1)) == -1) { - // current semester is over, we need to get student data again - callback.onActionStarted(R.string.sync_action_getting_account); - getStudentList(usersMap -> { - studentId = profile.getStudentData("studentId", -1); - d(TAG, "Searching the student ID "+studentId); - JsonObject foundUser = null; - for (JsonObject user: usersMap.values()) { - if (user.get("Id").getAsInt() == studentId) { - foundUser = user; - } - } - if (foundUser == null || foundUser.get("IdOkresKlasyfikacyjny").getAsInt() == 0) { - d(TAG, "Not found"); - finishWithError(new AppError(TAG, 507, AppError.CODE_OTHER, app.getString(R.string.error_register_student_no_term), usersMap.toString())); - } - else { - d(TAG, "Saving updated student data"); - saveStudentData(profile, foundUser); - loginCallback.onSuccess(); - } - }, true); - } - else { - loginCallback.onSuccess(); - } - } - } - - /* _ _ _ _ _ _ _ - | | | | | | ___ | | | | | | - | |__| | ___| |_ __ ___ _ __ ___ ( _ ) ___ __ _| | | |__ __ _ ___| | _____ - | __ |/ _ \ | '_ \ / _ \ '__/ __| / _ \/\ / __/ _` | | | '_ \ / _` |/ __| |/ / __| - | | | | __/ | |_) | __/ | \__ \ | (_> < | (_| (_| | | | |_) | (_| | (__| <\__ \ - |_| |_|\___|_| .__/ \___|_| |___/ \___/\/ \___\__,_|_|_|_.__/ \__,_|\___|_|\_\___/ - | | - |*/ - private void getCertificate(String token, String pin, CertificateCallback certificateCallback) { - callback.onActionStarted(R.string.sync_action_getting_certificate); - d(TAG, "Post JSON "+apiUrl + ENDPOINT_CERTIFICATE); - if (apiUrl == null) { - finishWithError(new AppError(TAG, 1215, AppError.CODE_INVALID_TOKEN, "Empty apiUrl(1)")); - return; - } - Request.builder() - .url(apiUrl + ENDPOINT_CERTIFICATE) - .userAgent(userAgent) - .addParameter("PIN", pin) - .addParameter("TokenKey", token) - .addParameter("AppVersion", MOBILE_APP_VERSION) - .addParameter("DeviceId", UUID.randomUUID().toString()) - .addParameter("DeviceName", "Szkolny.eu "+Build.MODEL) - .addParameter("DeviceNameUser", "") - .addParameter("DeviceDescription", "") - .addParameter("DeviceSystemType", "Android") - .addParameter("DeviceSystemVersion", Build.VERSION.RELEASE) - .addParameter("RemoteMobileTimeKey", getUnixTime()) - .addParameter("TimeKey", getUnixTime() - 1) - .addParameter("RequestId", UUID.randomUUID().toString()) - .addParameter("RemoteMobileAppVersion", MOBILE_APP_VERSION) - .addParameter("RemoteMobileAppName", MOBILE_APP_NAME) - .postJson() - .addHeader("RequestMobileType", "RegisterDevice") - .callback(new JsonCallbackHandler() { - @Override - public void onSuccess(JsonObject data, Response response) { - if (data == null) { - finishWithError(new AppError(TAG, 1241, AppError.CODE_MAINTENANCE, response)); - return; - } - d(TAG, "Certificate data "+data.toString()); - boolean isError = data.get("IsError").getAsBoolean(); - if (isError) { - JsonElement message = data.get("Message"); - JsonElement tokenStatus = data.get("TokenStatus"); - String msg = null; - String tokenStatusStr = null; - if (message != null) { - msg = message.getAsString(); - } - if (tokenStatus != null) { - tokenStatusStr = tokenStatus.getAsString(); - } - if ("TokenNotFound".equals(msg)) { - finishWithError(new AppError(TAG, 1258, AppError.CODE_INVALID_TOKEN, response, data)); - return; - } - if ("TokenDead".equals(msg)) { - finishWithError(new AppError(TAG, 1262, AppError.CODE_EXPIRED_TOKEN, response, data)); - return; - } - if ("WrongPIN".equals(tokenStatusStr)) { - Matcher matcher = Pattern.compile("Liczba pozostałych prób: ([0-9])", Pattern.DOTALL).matcher(tokenStatusStr); - finishWithError(new AppError(TAG, 1267, AppError.CODE_INVALID_PIN, matcher.matches() ? matcher.group(1) : "?", response, data)); - return; - } - if ("Broken".equals(tokenStatusStr)) { - finishWithError(new AppError(TAG, 1262, AppError.CODE_INVALID_PIN, "0", response, data)); - return; - } - finishWithError(new AppError(TAG, 1274, AppError.CODE_OTHER, "Sprawdź wprowadzone dane.\n\nBłąd certyfikatu "+(message == null ? "" : message.getAsString()), response, data)); - return; - } - JsonElement tokenCert = data.get("TokenCert"); - if (tokenCert == null || tokenCert instanceof JsonNull) { - finishWithError(new AppError(TAG, 1279, AppError.CODE_OTHER, "Sprawdź wprowadzone dane.\n\nCertificate error. TokenCert null", response, data)); - return; - } - data = tokenCert.getAsJsonObject(); - try { - certificateCallback.onSuccess( - data.get("CertyfikatKlucz").getAsString(), - data.get("CertyfikatPfx").getAsString(), - data.get("AdresBazowyRestApi").getAsString(), - data.get("UzytkownikLogin").getAsString(), - data.get("UzytkownikNazwa").getAsString() - ); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 1293, AppError.CODE_OTHER, response, e, data)); - } - } - - @Override - public void onFailure(Response response, Throwable throwable) { - if (response.code() == 400) { - finishWithError(new AppError(TAG, 1300, AppError.CODE_INVALID_SYMBOL, response, throwable)); - return; - } - finishWithError(new AppError(TAG, 1303, AppError.CODE_OTHER, response, throwable)); - } - }) - .build() - .enqueue(); - } - - private void apiRequest(String endpoint, JsonObject payload, ApiRequestCallback apiRequestCallback) { - d(TAG, "API request "+apiUrl + endpoint); - if (apiUrl == null) { - finishWithError(new AppError(TAG, 1313, AppError.CODE_OTHER, app.getString(R.string.sync_error_no_api_url), "Empty apiUrl(2)")); - return; - } - if (payload == null) - payload = new JsonObject(); - payload.addProperty("RemoteMobileTimeKey", getUnixTime()); - payload.addProperty("TimeKey", getUnixTime() - 1); - payload.addProperty("RequestId", UUID.randomUUID().toString()); - payload.addProperty("RemoteMobileAppVersion", MOBILE_APP_VERSION); - payload.addProperty("RemoteMobileAppName", MOBILE_APP_NAME); - Request.builder() - .url(apiUrl + endpoint) - .userAgent(userAgent) - .withClient(signingHttp) - .setJsonBody(payload) - .callback(new JsonCallbackHandler() { - @Override - public void onSuccess(JsonObject data, Response response) { - if (data == null) { - finishWithError(new AppError(TAG, 1332, AppError.CODE_MAINTENANCE, response)); - return; - } - try { - apiRequestCallback.onSuccess(data); - } - catch (Exception e) { - finishWithError(new AppError(TAG, 1339, AppError.CODE_OTHER, response, e, data)); - } - } - - @Override - public void onFailure(Response response, Throwable throwable) { - finishWithError(new AppError(TAG, 1345, AppError.CODE_OTHER, response, throwable)); - } - }) - .build() - .enqueue(); - } - - private void getStudentList(StudentListCallback studentListCallback, boolean reportStudentsWithNoSemester) { - apiRequest(ENDPOINT_STUDENT_LIST, null, data -> { - d(TAG, "Got: " + data.toString()); - JsonElement listEl = data.get("Data"); - JsonArray accountList; - if (listEl == null || listEl instanceof JsonNull || (accountList = listEl.getAsJsonArray()).size() == 0) { - finishWithError(new AppError(TAG, 1369, AppError.CODE_OTHER, app.getString(R.string.sync_error_register_no_students), data)); - return; - } - LinkedHashMap usersMap = new LinkedHashMap<>(); - for (JsonElement userEl : accountList) { - JsonObject user = userEl.getAsJsonObject(); - if (reportStudentsWithNoSemester || (user != null && user.get("IdOkresKlasyfikacyjny").getAsInt() != 0)) { - usersMap.put(user.get("Imie").getAsString() + " " + user.get("Nazwisko").getAsString() + " " + (user.get("OddzialKod") instanceof JsonNull ? "" : user.get("OddzialKod").getAsString()), user); - } else if (accountList.size() == 1) { - finishWithError(new AppError(TAG, 1377, AppError.CODE_OTHER, app.getString(R.string.error_register_student_no_term), data)); - return; - } - } - studentListCallback.onSuccess(usersMap); - }); - } - - private void saveStudentData(Profile targetProfile, JsonObject account) { - String firstName = account.get("Imie").getAsString(); - String lastName = account.get("Nazwisko").getAsString(); - schoolName = loginStore.getLoginData("deviceSymbol", getSymbolFromApiUrl())+"_"+account.get("JednostkaSprawozdawczaSymbol").getAsString(); - schoolSymbol = account.get("JednostkaSprawozdawczaSymbol").getAsString(); - studentId = account.get("Id").getAsInt(); - studentLoginId = account.get("UzytkownikLoginId").getAsInt(); - studentClassId = account.get("IdOddzial").getAsInt(); - studentSemesterId = account.get("IdOkresKlasyfikacyjny").getAsInt(); - String studentClassName = account.get("OkresPoziom").getAsInt()+account.get("OddzialSymbol").getAsString(); - targetProfile.putStudentData("userName", account.get("UzytkownikNazwa").getAsString()); - targetProfile.putStudentData("schoolName", schoolName); - targetProfile.putStudentData("schoolSymbol", schoolSymbol); - targetProfile.putStudentData("studentId", studentId); - targetProfile.putStudentData("studentLoginId", studentLoginId); - targetProfile.putStudentData("studentClassId", studentClassId); - targetProfile.putStudentData("studentClassName", studentClassName); - targetProfile.putStudentData("studentSemesterId", studentSemesterId); - targetProfile.putStudentData("currentSemesterEndDate", account.get("OkresDataDo").getAsInt()+86400); - targetProfile.putStudentData("studentSemesterNumber", account.get("OkresNumer").getAsInt()); - targetProfile.setCurrentSemester(account.get("OkresNumer").getAsInt()); - if (targetProfile.getCurrentSemester() == 1) { - targetProfile.setDateSemester1Start(Date.fromMillis((long) account.get("OkresDataOd").getAsInt() * 1000)); - targetProfile.setDateSemester2Start(Date.fromMillis(((long) account.get("OkresDataDo").getAsInt() + 86400) * 1000)); - } - else if (targetProfile.getCurrentSemester() == 2) { - targetProfile.setDateSemester2Start(Date.fromMillis((long) account.get("OkresDataOd").getAsInt() * 1000)); - targetProfile.setDateYearEnd(Date.fromMillis(((long) account.get("OkresDataDo").getAsInt() + 86400) * 1000)); - } - //db.teamDao().add(new Team(profileId, studentClassId, account.get("OddzialKod").getAsString(), 1, -1)); - targetProfile.setStudentNameLong(firstName + " " + lastName); - targetProfile.setStudentNameShort(firstName + " " + lastName.charAt(0) + "."); - if (targetProfile.getName() == null || targetProfile.getName().equals("")) { - targetProfile.setName(targetProfile.getStudentNameLong()); - } - targetProfile.setSubname(account.get("UzytkownikLogin").getAsString()); - } - - private Team searchTeam(String name, String code, long teacherId) { - Team team; - team = Team.getByName(teamList, name); - - if (team == null) { - team = new Team(profileId, crc16(name.getBytes()), name, 2, code, teacherId); - teamList.add(team); - } - else { - team.teacherId = teacherId; - } - return team; - } - - public interface CertificateCallback { - void onSuccess(String certificateKey, String certificatePfx, String apiUrl, String userLogin, String userName); - } - private interface StudentListCallback { - void onSuccess(LinkedHashMap usersMap); - } - private interface ApiRequestCallback { - void onSuccess(JsonObject data); - } - - private String getSymbolFromApiUrl() { - String[] parts = apiUrl.split("/"); - return parts[parts.length - 2]; - } - - private String getClassTeamName() { - if (teamList.size() == 0) - return ""; - if (teamList.get(0).type != 1) - return ""; - return teamList.get(0).name; - } - - private long getClassTeamId() { - if (teamList.size() == 0) - return -1; - if (teamList.get(0).type != 1) - return -1; - return teamList.get(0).id; - } - - private Date getCurrentSemesterStartDate() { - return profile.getSemesterStart(studentSemesterNumber); - } - - private Date getCurrentSemesterEndDate() { - return profile.getSemesterEnd(studentSemesterNumber); - } - - private long getUnixTime() { - return System.currentTimeMillis() / 1000; - } - - private void setApiUrl(String token, String symbol) { - String rule = token.substring(0, 3); - switch (rule) { - case "3S1": - if (!App.devMode) - apiUrl = "https://lekcjaplus.vulcan.net.pl/"+symbol+"/"; - else - apiUrl = "http://hack.szkolny.eu/"+symbol+"/"; - break; - case "TA1": - apiUrl = "https://uonetplus-komunikacja.umt.tarnow.pl/"+symbol+"/"; - break; - case "OP1": - apiUrl = "https://uonetplus-komunikacja.eszkola.opolskie.pl/"+symbol+"/"; - break; - case "RZ1": - apiUrl = "https://uonetplus-komunikacja.resman.pl/"+symbol+"/"; - break; - case "GD1": - apiUrl = "https://uonetplus-komunikacja.edu.gdansk.pl/"+symbol+"/"; - break; - case "KA1": - apiUrl = "https://uonetplus-komunikacja.mcuw.katowice.eu/"+symbol+"/"; - break; - case "KA2": - apiUrl = "https://uonetplus-komunikacja-test.mcuw.katowice.eu/"+symbol+"/"; - break; - case "P03": - apiUrl = "https://efeb-komunikacja-pro-efebmobile.pro.vulcan.pl/"+symbol+"/"; - break; - case "P01": - apiUrl = "http://efeb-komunikacja.pro-hudson.win.vulcan.pl/"+symbol+"/"; - break; - case "P02": - apiUrl = "http://efeb-komunikacja.pro-hudsonrc.win.vulcan.pl/"+symbol+"/"; - break; - case "P90": - apiUrl = "http://efeb-komunikacja-pro-mwujakowska.neo.win.vulcan.pl/"+symbol+"/"; - break; - case "FK1": - apiUrl = "http://api.fakelog.cf/"+symbol+"/"; - break; - case "SZ9": - //apiUrl = "http://vulcan.szkolny.eu/"+symbol+"/"; - break; - default: - apiUrl = null; - break; - } - } - - /* _____ _ _____ _ - | __ \ | | | __ \ | | - | | | | __ _| |_ __ _ | |__) |___ __ _ _ _ ___ ___| |_ ___ - | | | |/ _` | __/ _` | | _ // _ \/ _` | | | |/ _ \/ __| __/ __| - | |__| | (_| | || (_| | | | \ \ __/ (_| | |_| | __/\__ \ |_\__ \ - |_____/ \__,_|\__\__,_| |_| \_\___|\__, |\__,_|\___||___/\__|___/ - | | - |*/ - private void setPushToken() { - String token; - Pair> pair = app.appConfig.fcmTokens.get(LOGIN_TYPE_VULCAN); - if (pair == null || (token = pair.first) == null || (pair.second != null && pair.second.contains(profileId))) { - r("finish", "SetPushToken"); - return; - } - callback.onActionStarted(R.string.sync_action_setting_push_token); - JsonObject json = new JsonObject(); - json.addProperty("IdUczen", studentId); - json.addProperty("Token", token); - json.addProperty("PushOcena", true); - json.addProperty("PushFrekwencja", true); - json.addProperty("PushUwaga", true); - json.addProperty("PushWiadomosc", true); - apiRequest(schoolSymbol+"/"+ENDPOINT_PUSH, json, result -> { - if (pair.second == null) { - List list = new ArrayList<>(); - list.add(profileId); - app.appConfig.fcmTokens.put(LOGIN_TYPE_VULCAN, new Pair<>(token, list)); - } - else { - pair.second.add(profileId); - app.appConfig.fcmTokens.put(LOGIN_TYPE_VULCAN, new Pair<>(token, pair.second)); - } - r("finish", "SetPushToken"); - }); - } - - private SparseArray> lessonRanges = new SparseArray<>(); - private SparseArray noticeCategories = new SparseArray<>(); - private SparseArray> attendanceCategories = new SparseArray<>(); - private void getDictionaries() { - callback.onActionStarted(R.string.sync_action_syncing_dictionaries); - if (teamList.size() == 0 || teamList.get(0).type != 1) { - String name = profile.getStudentData("studentClassName", null); - if (name != null) { - long id = crc16(name.getBytes()); - teamList.add(new Team(profileId, id, name, 1, schoolName + ":" + name, -1)); - } - } - apiRequest(schoolSymbol+"/"+ENDPOINT_DICTIONARIES, null, result -> { - JsonObject data = result.getAsJsonObject("Data"); - JsonArray teachers = data.getAsJsonArray("Nauczyciele"); - JsonArray subjects = data.getAsJsonArray("Przedmioty"); - JsonArray lessonRanges = data.getAsJsonArray("PoryLekcji"); - JsonArray gradeCategories = data.getAsJsonArray("KategorieOcen"); - JsonArray noticeCategories = data.getAsJsonArray("KategorieUwag"); - JsonArray attendanceCategories = data.getAsJsonArray("KategorieFrekwencji"); - - for (JsonElement teacherEl : teachers) { - JsonObject teacher = teacherEl.getAsJsonObject(); - JsonElement loginId = teacher.get("LoginId"); - teacherList.add( - new Teacher( - profileId, - teacher.get("Id").getAsInt(), - teacher.get("Imie").getAsString(), - teacher.get("Nazwisko").getAsString(), - loginId == null || loginId instanceof JsonNull ? "-1" : loginId.getAsString() - ) - ); - } - - for (JsonElement subjectEl : subjects) { - JsonObject subject = subjectEl.getAsJsonObject(); - subjectList.add( - new Subject( - profileId, - subject.get("Id").getAsInt(), - subject.get("Nazwa").getAsString(), - subject.get("Kod").getAsString() - ) - ); - } - - this.lessonRanges.clear(); - for (JsonElement lessonRangeEl : lessonRanges) { - JsonObject lessonRange = lessonRangeEl.getAsJsonObject(); - this.lessonRanges.put( - lessonRange.get("Id").getAsInt(), - new Pair<>( - Time.fromH_m(lessonRange.get("PoczatekTekst").getAsString()), - Time.fromH_m(lessonRange.get("KoniecTekst").getAsString()) - ) - ); - } - - for (JsonElement gradeCategoryEl : gradeCategories) { - JsonObject gradeCategory = gradeCategoryEl.getAsJsonObject(); - gradeCategoryList.add( - new GradeCategory( - profileId, - gradeCategory.get("Id").getAsInt(), - -1, - -1, - gradeCategory.get("Nazwa").getAsString() - ) - ); - } - - this.noticeCategories.clear(); - for (JsonElement noticeCategoryEl : noticeCategories) { - JsonObject noticeCategory = noticeCategoryEl.getAsJsonObject(); - this.noticeCategories.put( - noticeCategory.get("Id").getAsInt(), - noticeCategory.get("Nazwa").getAsString() - ); - } - - this.attendanceCategories.clear(); - for (JsonElement attendanceCategoryEl : attendanceCategories) { - JsonObject attendanceCategory = attendanceCategoryEl.getAsJsonObject(); - int type = -1; - boolean absent = attendanceCategory.get("Nieobecnosc").getAsBoolean(); - boolean excused = attendanceCategory.get("Usprawiedliwione").getAsBoolean(); - if (absent) { - type = excused ? TYPE_ABSENT_EXCUSED : TYPE_ABSENT; - } - else { - if (attendanceCategory.get("Spoznienie").getAsBoolean()) { - type = excused ? TYPE_BELATED_EXCUSED : TYPE_BELATED; - } - else if (attendanceCategory.get("Zwolnienie").getAsBoolean()) { - type = TYPE_RELEASED; - } - else if (attendanceCategory.get("Obecnosc").getAsBoolean()) { - type = TYPE_PRESENT; - } - } - this.attendanceCategories.put( - attendanceCategory.get("Id").getAsInt(), - new Pair<>( - type, - attendanceCategory.get("Nazwa").getAsString() - ) - ); - } - r("finish", "Dictionaries"); - }); - } - - private void getTimetable() { - if (syncingSemester1) { - // we are in semester 2. we're syncing semester 1 to show old data. skip the timetable. - r("finish", "Timetable"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_timetable); - JsonObject json = new JsonObject(); - json.addProperty("IdUczen", studentId); - - Date weekStart = Week.getWeekStart(); - if (Date.getToday().getWeekDay() > 4) { - weekStart.stepForward(0, 0, 7); - } - Date weekEnd = weekStart.clone().stepForward(0, 0, 6); - - json.addProperty("DataPoczatkowa", weekStart.getStringY_m_d()); - json.addProperty("DataKoncowa", weekEnd.getStringY_m_d()); - json.addProperty("IdOddzial", studentClassId); - json.addProperty("IdOkresKlasyfikacyjny", studentSemesterId); - apiRequest(schoolSymbol+"/"+ENDPOINT_TIMETABLE, json, result -> { - JsonArray lessons = result.getAsJsonArray("Data"); - - for (JsonElement lessonEl: lessons) { - JsonObject lesson = lessonEl.getAsJsonObject(); - - if (!lesson.get("PlanUcznia").getAsBoolean()) { - continue; - } - - Date lessonDate = Date.fromY_m_d(lesson.get("DzienTekst").getAsString()); - Pair lessonRange = lessonRanges.get(lesson.get("IdPoraLekcji").getAsInt()); - Lesson lessonObject = new Lesson( - profileId, - lessonDate.getWeekDay(), - lessonRange.first, - lessonRange.second - ); - - JsonElement subject = lesson.get("IdPrzedmiot"); - if (!(subject instanceof JsonNull)) { - lessonObject.subjectId = subject.getAsInt(); - } - JsonElement teacher = lesson.get("IdPracownik"); - if (!(teacher instanceof JsonNull)) { - lessonObject.teacherId = teacher.getAsInt(); - } - - lessonObject.teamId = getClassTeamId(); - JsonElement team = lesson.get("PodzialSkrot"); - if (team != null && !(team instanceof JsonNull)) { - String name = getClassTeamName()+" "+team.getAsString(); - Team teamObject = searchTeam(name, schoolName+":"+name, lessonObject.teacherId); - lessonObject.teamId = teamObject.id; - } - JsonElement classroom = lesson.get("Sala"); - if (classroom != null && !(classroom instanceof JsonNull)) { - lessonObject.classroomName = classroom.getAsString(); - } - - JsonElement isCancelled = lesson.get("PrzekreslonaNazwa"); - JsonElement oldTeacherId = lesson.get("IdPracownikOld"); - if (isCancelled != null && isCancelled.getAsBoolean()) { - LessonChange lessonChangeObject = new LessonChange( - profileId, - lessonDate, lessonObject.startTime, lessonObject.endTime - ); - lessonChangeObject.type = TYPE_CANCELLED; - lessonChangeObject.teacherId = lessonObject.teacherId; - lessonChangeObject.subjectId = lessonObject.subjectId; - lessonChangeObject.teamId = lessonObject.teamId; - lessonChangeObject.classroomName = lessonObject.classroomName; - - lessonChangeList.add(lessonChangeObject); - metadataList.add(new Metadata(profileId,Metadata.TYPE_LESSON_CHANGE, lessonChangeObject.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - else if (!(oldTeacherId instanceof JsonNull)) { - LessonChange lessonChangeObject = new LessonChange( - profileId, - lessonDate, lessonObject.startTime, lessonObject.endTime - ); - lessonChangeObject.type = TYPE_CHANGE; - lessonChangeObject.teacherId = lessonObject.teacherId; - lessonChangeObject.subjectId = lessonObject.subjectId; - lessonChangeObject.teamId = lessonObject.teamId; - lessonChangeObject.classroomName = lessonObject.classroomName; - lessonObject.teacherId = oldTeacherId.getAsInt(); - - lessonChangeList.add(lessonChangeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_LESSON_CHANGE, lessonChangeObject.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - lessonList.add(lessonObject); - } - r("finish", "Timetable"); - }); - } - - private void getGrades() { - callback.onActionStarted(R.string.sync_action_syncing_grades); - JsonObject json = new JsonObject(); - json.addProperty("IdUczen", studentId); - json.addProperty("IdOkresKlasyfikacyjny", studentSemesterId); - apiRequest(schoolSymbol+"/"+ENDPOINT_GRADES, json, result -> { - JsonArray grades = result.getAsJsonArray("Data"); - - for (JsonElement gradeEl: grades) { - JsonObject grade = gradeEl.getAsJsonObject(); - JsonElement name = grade.get("Wpis"); - JsonElement description = grade.get("Opis"); - JsonElement comment = grade.get("Komentarz"); - JsonElement value = grade.get("Wartosc"); - JsonElement modificatorValue = grade.get("WagaModyfikatora"); - JsonElement numerator = grade.get("Licznik"); - JsonElement denominator = grade.get("Mianownik"); - - int id = grade.get("Id").getAsInt(); - int weight = grade.get("WagaOceny").getAsInt(); - int subjectId = grade.get("IdPrzedmiot").getAsInt(); - int teacherId = grade.get("IdPracownikD").getAsInt(); - int categoryId = grade.get("IdKategoria").getAsInt(); - long addedDate = grade.get("DataModyfikacji").getAsLong() * 1000; - - float finalValue = 0.0f; - String finalName; - String finalDescription = ""; - - if (!(numerator instanceof JsonNull) && !(denominator instanceof JsonNull)) { - // calculate the grade's value and name: divide - float numeratorF = numerator.getAsFloat(); - float denominatorF = denominator.getAsFloat(); - finalValue = numeratorF / denominatorF; - weight = 0; - finalName = intToStr(Math.round(finalValue*100))+"%"; - finalDescription += new DecimalFormat("#.##").format(numeratorF)+"/"+new DecimalFormat("#.##").format(denominatorF); - } - else { - // "normal" grade. Get the name and value if set. Otherwise zero the weight. - finalName = name.getAsString(); - if (!(value instanceof JsonNull)) { - finalValue = value.getAsFloat(); - if (!(modificatorValue instanceof JsonNull)) { - finalValue += modificatorValue.getAsFloat(); - } - } - else { - weight = 0; - } - } - - if (!(comment instanceof JsonNull)) { - if (finalName.equals("")) { - finalName = comment.getAsString(); - } - else { - finalDescription += (finalDescription.equals("") ? "" : " ")+comment.getAsString(); - } - } - if (!(description instanceof JsonNull)) { - finalDescription += (finalDescription.equals("") ? "" : " - ")+description.getAsString(); - } - - int semester = studentSemesterNumber; - /*Date addedDateObj = Date.fromMillis(addedDate); - if (app.register.dateSemester2Start != null && addedDateObj.compareTo(app.register.dateSemester2Start) > 0) { - semester = 2; - }*/ - - String category = ""; - for (GradeCategory gradeCategory: gradeCategoryList) { - if (gradeCategory.categoryId == categoryId) { - category = gradeCategory.text; - } - } - - int color = 0xff3D5F9C; - switch (finalName) { - case "1-": - case "1": - case "1+": - color = 0xffd65757; - break; - case "2-": - case "2": - case "2+": - color = 0xff9071b3; - break; - case "3-": - case "3": - case "3+": - color = 0xffd2ab24; - break; - case "4-": - case "4": - case "4+": - color = 0xff50b6d6; - break; - case "5-": - case "5": - case "5+": - color = 0xff2cbd92; - break; - case "6-": - case "6": - case "6+": - color = 0xff91b43c; - break; - } - - Grade gradeObject = new Grade( - profileId, - id, - category, - color, - finalDescription, - finalName, - finalValue, - weight, - semester, - teacherId, - subjectId - ); - - gradeList.add(gradeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_GRADE, gradeObject.id, profile.getEmpty(), profile.getEmpty(), addedDate)); - } - r("finish", "Grades"); - }); - } - - private void processGradeList(JsonArray jsonGrades, List gradeList, List metadataList, boolean isFinal) { - for (JsonElement gradeEl: jsonGrades) { - JsonObject grade = gradeEl.getAsJsonObject(); - - String name = grade.get("Wpis").getAsString(); - float value = getGradeValue(name); - long subjectId = grade.get("IdPrzedmiot").getAsLong(); - - long id = subjectId * -100 - studentSemesterNumber; - - int color = getVulcanGradeColor(name); - - Grade gradeObject = new Grade( - profileId, - id, - "", - color, - "", - name, - value, - 0, - studentSemesterNumber, - -1, - subjectId - ); - if (studentSemesterNumber == 1) { - gradeObject.type = isFinal ? TYPE_SEMESTER1_FINAL : TYPE_SEMESTER1_PROPOSED; - } - else { - gradeObject.type = isFinal ? TYPE_SEMESTER2_FINAL : TYPE_SEMESTER2_PROPOSED; - } - gradeList.add(gradeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_GRADE, gradeObject.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - } - private void getProposedGrades() { - callback.onActionStarted(R.string.sync_action_syncing_proposition_grades); - JsonObject json = new JsonObject(); - json.addProperty("IdUczen", studentId); - json.addProperty("IdOkresKlasyfikacyjny", studentSemesterId); - apiRequest(schoolSymbol+"/"+ENDPOINT_GRADES_PROPOSITIONS, json, result -> { - JsonObject grades = result.getAsJsonObject("Data"); - JsonArray gradesProposed = grades.getAsJsonArray("OcenyPrzewidywane"); - JsonArray gradesFinal = grades.getAsJsonArray("OcenyKlasyfikacyjne"); - - processGradeList(gradesProposed, gradeList, metadataList, false); - processGradeList(gradesFinal, gradeList, metadataList, true); - r("finish", "ProposedGrades"); - }); - } - - private void getEvents() { - callback.onActionStarted(R.string.sync_action_syncing_exams); - JsonObject json = new JsonObject(); - // from today to the end of the current semester - // or if empty: from the beginning of the semester - json.addProperty("DataPoczatkowa", profile.getEmpty() ? getCurrentSemesterStartDate().getStringY_m_d() : oneMonthBack.getStringY_m_d()); - json.addProperty("DataKoncowa", getCurrentSemesterEndDate().getStringY_m_d()); - json.addProperty("IdOddzial", studentClassId); - json.addProperty("IdUczen", studentId); - json.addProperty("IdOkresKlasyfikacyjny", studentSemesterId); - apiRequest(schoolSymbol+"/"+ENDPOINT_EVENTS, json, result -> { - JsonArray events = result.getAsJsonArray("Data"); - - for (JsonElement eventEl: events) { - JsonObject event = eventEl.getAsJsonObject(); - - int id = event.get("Id").getAsInt(); - - int eventType = event.get("Rodzaj").getAsBoolean() ? TYPE_EXAM : TYPE_SHORT_QUIZ; - - int subjectId = event.get("IdPrzedmiot").getAsInt(); - - Date lessonDate = Date.fromY_m_d(event.get("DataTekst").getAsString()); - Time startTime = null; - - int weekDay = lessonDate.getWeekDay(); - for (Lesson lesson: lessonList) { - if (lesson.weekDay == weekDay && lesson.subjectId == subjectId) { - startTime = lesson.startTime; - } - } - - long teacherId = event.get("IdPracownik").getAsInt(); - - Event eventObject = new Event( - profileId, - id, - lessonDate, - startTime, - event.get("Opis").getAsString(), - -1, - eventType, - false, - teacherId, - subjectId, - getClassTeamId() - ); - - JsonElement team = event.get("PodzialSkrot"); - if (team != null && !(team instanceof JsonNull)) { - String name = getClassTeamName()+" "+team.getAsString(); - Team teamObject = searchTeam(name, schoolName+":"+name, teacherId); - eventObject.teamId = teamObject.id; - } - - eventList.add(eventObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_EVENT, eventObject.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - r("finish", "Events"); - }); - } - - private void getHomework() { - callback.onActionStarted(R.string.sync_action_syncing_homework); - JsonObject json = new JsonObject(); - json.addProperty("DataPoczatkowa", profile.getEmpty() ? getCurrentSemesterStartDate().getStringY_m_d() : oneMonthBack.getStringY_m_d()); - json.addProperty("DataKoncowa", getCurrentSemesterEndDate().getStringY_m_d()); - json.addProperty("IdOddzial", studentClassId); - json.addProperty("IdUczen", studentId); - json.addProperty("IdOkresKlasyfikacyjny", studentSemesterId); - apiRequest(schoolSymbol+"/"+ ENDPOINT_HOMEWORK, json, result -> { - JsonArray homeworkList = result.getAsJsonArray("Data"); - - for (JsonElement homeworkEl: homeworkList) { - JsonObject homework = homeworkEl.getAsJsonObject(); - - int id = homework.get("Id").getAsInt(); - - int subjectId = homework.get("IdPrzedmiot").getAsInt(); - - Date lessonDate = Date.fromY_m_d(homework.get("DataTekst").getAsString()); - Time startTime = null; - - int weekDay = lessonDate.getWeekDay(); - for (Lesson lesson: lessonList) { - if (lesson.weekDay == weekDay && lesson.subjectId == subjectId) { - startTime = lesson.startTime; - } - } - - Event eventObject = new Event( - profileId, - id, - lessonDate, - startTime, - homework.get("Opis").getAsString(), - -1, - Event.TYPE_HOMEWORK, - false, - homework.get("IdPracownik").getAsInt(), - subjectId, - getClassTeamId() - ); - - eventList.add(eventObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_HOMEWORK, eventObject.id, profile.getEmpty(), profile.getEmpty(), System.currentTimeMillis())); - } - r("finish", "Homework"); - }); - } - - private void getNotices() { - callback.onActionStarted(R.string.sync_action_syncing_notices); - JsonObject json = new JsonObject(); - json.addProperty("IdUczen", studentId); - json.addProperty("IdOkresKlasyfikacyjny", studentSemesterId); - apiRequest(schoolSymbol+"/"+ENDPOINT_NOTICES, json, result -> { - JsonArray notices = result.getAsJsonArray("Data"); - - for (JsonElement noticeEl: notices) { - JsonObject notice = noticeEl.getAsJsonObject(); - - int id = notice.get("Id").getAsInt(); - long addedDate = Date.fromY_m_d(notice.get("DataWpisuTekst").getAsString()).getInMillis(); - - int semester = studentSemesterNumber; - /*Date addedDateObj = Date.fromMillis(addedDate); - if (profile.dateSemester2Start != null && addedDateObj.compareTo(profile.dateSemester2Start) > 0) { - semester = 2; - }*/ - - Notice noticeObject = new Notice( - profileId, - id, - (notice.get("IdKategoriaUwag") instanceof JsonNull ? "" : noticeCategories.get(notice.get("IdKategoriaUwag").getAsInt())) +"\n"+notice.get("TrescUwagi").getAsString(), - semester, - TYPE_NEUTRAL, - notice.get("IdPracownik").getAsInt() - ); - - noticeList.add(noticeObject); - metadataList.add(new Metadata(profileId, Metadata.TYPE_NOTICE, noticeObject.id, profile.getEmpty(), profile.getEmpty(), addedDate)); - } - r("finish", "Notices"); - }); - } - - private void getAttendance() { - callback.onActionStarted(R.string.sync_action_syncing_attendance); - JsonObject json = new JsonObject(); - json.addProperty("DataPoczatkowa", true ? getCurrentSemesterStartDate().getStringY_m_d() : oneMonthBack.getStringY_m_d()); - json.addProperty("DataKoncowa", getCurrentSemesterEndDate().getStringY_m_d()); - json.addProperty("IdOddzial", studentClassId); - json.addProperty("IdUczen", studentId); - json.addProperty("IdOkresKlasyfikacyjny", studentSemesterId); - apiRequest(schoolSymbol+"/"+ ENDPOINT_ATTENDANCE, json, result -> { - JsonArray attendanceList = result.getAsJsonObject("Data").getAsJsonArray("Frekwencje"); - - for (JsonElement attendanceEl: attendanceList) { - JsonObject attendance = attendanceEl.getAsJsonObject(); - - Pair attendanceCategory = attendanceCategories.get(attendance.get("IdKategoria").getAsInt()); - if (attendanceCategory == null) - continue; - - int type = attendanceCategory.first; - - int id = attendance.get("Dzien").getAsInt() + attendance.get("Numer").getAsInt(); - - long lessonDateMillis = Date.fromY_m_d(attendance.get("DzienTekst").getAsString()).getInMillis(); - Date lessonDate = Date.fromMillis(lessonDateMillis); - - int lessonSemester = profile.dateToSemester(lessonDate); - - Attendance attendanceObject = new Attendance( - profileId, - id, - 0, - attendance.get("IdPrzedmiot").getAsInt(), - lessonSemester, - attendance.get("PrzedmiotNazwa").getAsString()+" - "+attendanceCategory.second, - lessonDate, - lessonRanges.get(attendance.get("IdPoraLekcji").getAsInt()).first, - type); - - this.attendanceList.add(attendanceObject); - if (attendanceObject.type != TYPE_PRESENT) { - metadataList.add(new Metadata(profileId, Metadata.TYPE_ATTENDANCE, attendanceObject.id, profile.getEmpty(), profile.getEmpty(), attendanceObject.lessonDate.combineWith(attendanceObject.startTime))); - } - } - r("finish", "Attendance"); - }); - } - - private void getMessagesInbox() { - callback.onActionStarted(R.string.sync_action_syncing_messages_inbox); - JsonObject json = new JsonObject(); - json.addProperty("DataPoczatkowa", true ? getCurrentSemesterStartDate().getInUnix() : oneMonthBack.getInUnix()); - json.addProperty("DataKoncowa", getCurrentSemesterEndDate().getInUnix()); - json.addProperty("LoginId", studentLoginId); - json.addProperty("IdUczen", studentId); - apiRequest(schoolSymbol+"/"+ENDPOINT_MESSAGES_RECEIVED, json, result -> { - JsonArray messages = result.getAsJsonArray("Data"); - - for (JsonElement messageEl: messages) { - JsonObject message = messageEl.getAsJsonObject(); - - long id = message.get("WiadomoscId").getAsLong(); - - long senderId = -1; - int senderLoginId = message.get("NadawcaId").getAsInt(); - String senderLoginIdStr = String.valueOf(senderLoginId); - - for (Teacher teacher: teacherList) { - if (senderLoginIdStr.equals(teacher.loginId)) { - senderId = teacher.id; - } - } - - String subject = message.get("Tytul").getAsString(); - String body = message.get("Tresc").getAsString(); - body = body.replaceAll("\n", "
    "); - - long addedDate = message.get("DataWyslaniaUnixEpoch").getAsLong() * 1000; - long readDate = 0; - JsonElement readDateUnix; - if (!((readDateUnix = message.get("DataPrzeczytaniaUnixEpoch")) instanceof JsonNull)) { - readDate = readDateUnix.getAsLong() * 1000L; - } - - Message messageObject = new Message(profileId, id, subject, body, TYPE_RECEIVED, senderId, -1); - MessageRecipient messageRecipientObject = new MessageRecipient(profileId, -1, -1, readDate, id); - - messageList.add(messageObject); - messageRecipientList.add(messageRecipientObject); - messageMetadataList.add(new Metadata(profileId, TYPE_MESSAGE, id, readDate > 0, readDate > 0 || profile.getEmpty(), addedDate)); - } - r("finish", "MessagesInbox"); - }); - } - - private void getMessagesOutbox() { - if (!fullSync && onlyFeature != FEATURE_MESSAGES_OUTBOX) { - r("finish", "MessagesOutbox"); - return; - } - callback.onActionStarted(R.string.sync_action_syncing_messages_outbox); - JsonObject json = new JsonObject(); - json.addProperty("DataPoczatkowa", true ? getCurrentSemesterStartDate().getInUnix() : oneMonthBack.getInUnix()); - json.addProperty("DataKoncowa", getCurrentSemesterEndDate().getInUnix()); - json.addProperty("LoginId", studentLoginId); - json.addProperty("IdUczen", studentId); - apiRequest(schoolSymbol+"/"+ENDPOINT_MESSAGES_SENT, json, result -> { - JsonArray messages = result.getAsJsonArray("Data"); - - for (JsonElement jMessageEl: messages) { - JsonObject jMessage = jMessageEl.getAsJsonObject(); - - long messageId = jMessage.get("WiadomoscId").getAsLong(); - - String subject = jMessage.get("Tytul").getAsString(); - - String body = jMessage.get("Tresc").getAsString(); - body = body.replaceAll("\n", "
    "); - - long sentDate = jMessage.get("DataWyslaniaUnixEpoch").getAsLong() * 1000; - - Message message = new Message( - profileId, - messageId, - subject, - body, - TYPE_SENT, - -1, - -1 - ); - - int readBy = jMessage.get("Przeczytane").getAsInt(); - int unreadBy = jMessage.get("Nieprzeczytane").getAsInt(); - for (JsonElement recipientEl: jMessage.getAsJsonArray("Adresaci")) { - JsonObject recipient = recipientEl.getAsJsonObject(); - long recipientId = -1; - int recipientLoginId = recipient.get("LoginId").getAsInt(); - String recipientLoginIdStr = String.valueOf(recipientLoginId); - for (Teacher teacher: teacherList) { - if (recipientLoginIdStr.equals(teacher.loginId)) { - recipientId = teacher.id; - } - } - MessageRecipient messageRecipient = new MessageRecipient( - profileId, - recipientId, - -1, - readBy == 0 ? 0 : unreadBy == 0 ? 1 : -1, - /*messageId*/ messageId - ); - messageRecipientIgnoreList.add(messageRecipient); - } - - messageList.add(message); - metadataList.add(new Metadata(profileId, Metadata.TYPE_MESSAGE, messageId, true, true, sentDate)); - } - r("finish", "MessagesOutbox"); - }); - } - - /* __ __ - | \/ | - | \ / | ___ ___ ___ __ _ __ _ ___ ___ - | |\/| |/ _ \/ __/ __|/ _` |/ _` |/ _ \/ __| - | | | | __/\__ \__ \ (_| | (_| | __/\__ \ - |_| |_|\___||___/___/\__,_|\__, |\___||___/ - __/ | - |__*/ - @Override - public void getMessage(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull MessageFull message, @NonNull MessageGetCallback messageCallback) { - if (message.body != null) { - message.recipients = app.db.messageRecipientDao().getAllByMessageId(profile.getId(), message.id); - for (MessageRecipientFull recipient: message.recipients) { - if (recipient.id == -1) - recipient.fullName = profile.getStudentNameLong(); - } - if (!message.seen) { - studentId = profile.getStudentData("studentId", -1); - studentLoginId = profile.getStudentData("studentLoginId", -1); - JsonObject json = new JsonObject(); - json.addProperty("WiadomoscId", message.id); - json.addProperty("FolderWiadomosci", "Odebrane"); - json.addProperty("Status", "Widoczna"); - json.addProperty("LoginId", studentLoginId); - json.addProperty("IdUczen", studentId); - apiRequest(schoolSymbol+"/"+ENDPOINT_MESSAGES_CHANGE_STATUS, json, result -> { }); - app.db.metadataDao().setSeen(profile.getId(), message, true); - if (message.type != TYPE_SENT) { - app.db.messageRecipientDao().add(new MessageRecipient(profile.getId(), -1, -1, System.currentTimeMillis(), message.id)); - } - } - new Handler(activityContext.getMainLooper()).post(() -> { - messageCallback.onSuccess(message); - }); - return; - } - } - - @Override - public void getAttachment(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull MessageFull message, long attachmentId, @NonNull AttachmentGetCallback attachmentCallback) { - - } - - @Override - public void getRecipientList(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull RecipientListGetCallback recipientListGetCallback) { - - } - - @Override - public MessagesComposeInfo getComposeInfo(@NonNull ProfileFull profile) { - return new MessagesComposeInfo(0, 0, -1, -1); - } - - @Override - public void syncMessages(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile) { - - } - - @Override - public Map getConfigurableEndpoints(Profile profile) { - return null; - } - - @Override - public boolean isEndpointEnabled(Profile profile, boolean defaultActive, String name) { - return defaultActive; - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/DataEdudziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/DataEdudziennik.kt new file mode 100644 index 00000000..d6aa2881 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/DataEdudziennik.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-22 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_EDUDZIENNIK_WEB +import pl.szczodrzynski.edziennik.data.api.models.Data +import pl.szczodrzynski.edziennik.data.db.modules.events.EventType +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher + +/** + * Use http://patorjk.com/software/taag/#p=display&f=Big for the ascii art + * + * Use https://codepen.io/kubasz/pen/RwwwbGN to easily generate the student data getters/setters + */ +class DataEdudziennik(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { + + fun isWebLoginValid() = webSessionIdExpiryTime-30 > currentTimeUnix() && webSessionId.isNotNullNorEmpty() + + override fun satisfyLoginMethods() { + loginMethods.clear() + if (isWebLoginValid()) { + loginMethods += LOGIN_METHOD_EDUDZIENNIK_WEB + } + } + + private var mLoginEmail: String? = null + var loginEmail: String? + get() { mLoginEmail = mLoginEmail ?: loginStore.getLoginData("email", null); return mLoginEmail } + set(value) { loginStore.putLoginData("email", value); mLoginEmail = value } + + private var mLoginPassword: String? = null + var loginPassword: String? + get() { mLoginPassword = mLoginPassword ?: loginStore.getLoginData("password", null); return mLoginPassword } + set(value) { loginStore.putLoginData("password", value); mLoginPassword = value } + + private var mStudentId: String? = null + var studentId: String? + get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", null); return mStudentId } + set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } + + private var mSchoolId: String? = null + var schoolId: String? + get() { mSchoolId = mSchoolId ?: profile?.getStudentData("schoolId", null); return mSchoolId } + set(value) { profile?.putStudentData("schoolId", value) ?: return; mSchoolId = value } + + private var mClassId: String? = null + var classId: String? + get() { mClassId = mClassId ?: profile?.getStudentData("classId", null); return mClassId } + set(value) { profile?.putStudentData("classId", value) ?: return; mClassId = value } + + /* __ __ _ + \ \ / / | | + \ \ /\ / /__| |__ + \ \/ \/ / _ \ '_ \ + \ /\ / __/ |_) | + \/ \/ \___|_._*/ + private var mWebSessionId: String? = null + var webSessionId: String? + get() { mWebSessionId = mWebSessionId ?: loginStore.getLoginData("webSessionId", null); return mWebSessionId } + set(value) { loginStore.putLoginData("webSessionId", value); mWebSessionId = value } + + private var mWebSessionIdExpiryTime: Long? = null + var webSessionIdExpiryTime: Long + get() { mWebSessionIdExpiryTime = mWebSessionIdExpiryTime ?: loginStore.getLoginData("webSessionIdExpiryTime", 0L); return mWebSessionIdExpiryTime ?: 0L } + set(value) { loginStore.putLoginData("webSessionIdExpiryTime", value); mWebSessionIdExpiryTime = value } + + /* ____ _ _ + / __ \| | | | + | | | | |_| |__ ___ _ __ + | | | | __| '_ \ / _ \ '__| + | |__| | |_| | | | __/ | + \____/ \__|_| |_|\___|*/ + private var mSchoolName: String? = null + var schoolName: String? + get() { mSchoolName = mSchoolName ?: profile?.getStudentData("schoolName", null); return mSchoolName } + set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolName = value } + + private var mTimetableNotPublic: Boolean? = null + var timetableNotPublic: Boolean + get() { mTimetableNotPublic = mTimetableNotPublic ?: profile?.getStudentData("timetableNotPublic", false); return mTimetableNotPublic ?: false } + set(value) { profile?.putStudentData("timetableNotPublic", value) ?: return; mTimetableNotPublic = value } + + val studentEndpoint: String + get() = "Students/$studentId/" + + val schoolEndpoint: String + get() = "Schools/$schoolId/" + + val classStudentEndpoint: String + get() = "Class/$studentId/" + + val schoolClassEndpoint: String + get() = "Schools/$classId/" + + val studentAndClassEndpoint: String + get() = "Students/$studentId/Klass/$classId/" + + val studentAndClassesEndpoint: String + get() = "Students/$studentId/Classes/$classId/" + + val timetableEndpoint: String + get() = "Plan/$studentId/" + + val studentAndTeacherClassEndpoint: String + get() = "Students/$studentId/Teachers/$classId/" + + val courseStudentEndpoint: String + get() = "Course/$studentId/" + + fun getSubject(longId: String, name: String): Subject { + val id = longId.crc32() + return subjectList.singleOrNull { it.id == id } ?: run { + val subject = Subject(profileId, id, name, name) + subjectList.put(id, subject) + subject + } + } + + fun getTeacher(firstName: String, lastName: String, longId: String? = null): Teacher { + val name = "$firstName $lastName".fixName() + val id = name.crc32() + return teacherList.singleOrNull { it.id == id }?.also { + if (longId != null && it.loginId == null) it.loginId = longId + } ?: run { + val teacher = Teacher(profileId, id, firstName, lastName, longId) + teacherList.put(id, teacher) + teacher + } + } + + fun getTeacherByFirstLast(nameFirstLast: String, longId: String? = null): Teacher { + val nameParts = nameFirstLast.split(" ") + return getTeacher(nameParts[0], nameParts[1], longId) + } + + fun getTeacherByLastFirst(nameLastFirst: String, longId: String? = null): Teacher { + val nameParts = nameLastFirst.split(" ") + return getTeacher(nameParts[1], nameParts[0], longId) + } + + fun getEventType(longId: String, name: String): EventType { + val id = longId.crc16().toLong() + return eventTypes.singleOrNull { it.id == id } ?: run { + val eventType = EventType(profileId, id, name, colorFromName(name)) + eventTypes.put(id, eventType) + eventType + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/Edudziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/Edudziennik.kt new file mode 100644 index 00000000..697beacc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/Edudziennik.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-22 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikData +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web.EdudziennikWebGetAnnouncement +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.firstlogin.EdudziennikFirstLogin +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.EdudziennikLogin +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.EdudziennikLoginWeb +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.utils.Utils.d + +class Edudziennik(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface { + companion object { + private const val TAG = "Edudziennik" + } + + val internalErrorList = mutableListOf() + val data: DataEdudziennik + private var afterLogin: (() -> Unit)? = null + + init { + data = DataEdudziennik(app, profile, loginStore).apply { + callback = wrapCallback(this@Edudziennik.callback) + satisfyLoginMethods() + } + } + + private fun completed() { + data.saveData() + data.notify { + callback.onCompleted() + } + } + + /* _______ _ _ _ _ _ + |__ __| | /\ | | (_) | | | + | | | |__ ___ / \ | | __ _ ___ _ __ _| |_| |__ _ __ ___ + | | | '_ \ / _ \ / /\ \ | |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \ + | | | | | | __/ / ____ \| | (_| | (_) | | | | |_| | | | | | | | | + |_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| + __/ | + |__*/ + override fun sync(featureIds: List, viewId: Int?, arguments: JsonObject?) { + data.arguments = arguments + data.prepare(edudziennikLoginMethods, EdudziennikFeatures, featureIds, viewId) + login() + } + + private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) { + d(TAG, "Trying to login with ${data.targetLoginMethodIds}") + if (internalErrorList.isNotEmpty()) { + d(TAG, " - Internal errors:") + internalErrorList.forEach { d(TAG, " - code $it") } + } + loginMethodId?.let { data.prepareFor(edudziennikLoginMethods, it) } + afterLogin?.let { this.afterLogin = it } + EdudziennikLogin(data) { + data() + } + } + + private fun data() { + d(TAG, "Endpoint IDs: ${data.targetEndpointIds}") + if (internalErrorList.isNotEmpty()) { + d(TAG, " - Internal errors:") + internalErrorList.forEach { d(TAG, " - code $it") } + } + afterLogin?.invoke() ?: EdudziennikData(data) { + completed() + } + } + + override fun getMessage(message: MessageFull) {} + override fun sendMessage(recipients: List, subject: String, text: String) {} + override fun markAllAnnouncementsAsRead() {} + + override fun getAnnouncement(announcement: AnnouncementFull) { + EdudziennikLoginWeb(data) { + EdudziennikWebGetAnnouncement(data, announcement) { + completed() + } + } + } + + override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) {} + override fun getRecipientList() {} + + override fun firstLogin() { EdudziennikFirstLogin(data) { completed() } } + override fun cancel() { + d(TAG, "Cancelled") + data.cancel() + } + + private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { + return object : EdziennikCallback { + override fun onCompleted() { callback.onCompleted() } + override fun onProgress(step: Float) { callback.onProgress(step) } + override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) } + override fun onError(apiError: ApiError) { + if (apiError.errorCode in internalErrorList) { + // finish immediately if the same error occurs twice during the same sync + callback.onError(apiError) + return + } + internalErrorList.add(apiError.errorCode) + when (apiError.errorCode) { + ERROR_EDUDZIENNIK_WEB_SESSION_EXPIRED -> { + login() + } + ERROR_LOGIN_EDUDZIENNIK_WEB_NO_SESSION_ID -> { + login() + } + ERROR_EDUDZIENNIK_WEB_TIMETABLE_NOT_PUBLIC -> { + data.timetableNotPublic = true + data() + } + else -> callback.onError(apiError) + } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/EdudziennikFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/EdudziennikFeatures.kt new file mode 100644 index 00000000..c3fd17ed --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/EdudziennikFeatures.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-23 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik + +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.models.Feature + +const val ENDPOINT_EDUDZIENNIK_WEB_START = 1000 +const val ENDPOINT_EDUDZIENNIK_WEB_TEACHERS = 1001 +const val ENDPOINT_EDUDZIENNIK_WEB_GRADES = 1011 +const val ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE = 1012 +const val ENDPOINT_EDUDZIENNIK_WEB_EXAMS = 1013 +const val ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE = 1014 +const val ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS = 1015 +const val ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK = 1016 +const val ENDPOINT_EDUDZIENNIK_WEB_EVENTS = 1017 +const val ENDPOINT_EDUDZIENNIK_WEB_NOTES = 1018 +const val ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER = 1030 + +val EdudziennikFeatures = listOf( + /* School and team info and subjects */ + Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_STUDENT_INFO, listOf( + ENDPOINT_EDUDZIENNIK_WEB_START to LOGIN_METHOD_EDUDZIENNIK_WEB + ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), + + /* Teachers */ + Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_TEACHERS, listOf( + ENDPOINT_EDUDZIENNIK_WEB_TEACHERS to LOGIN_METHOD_EDUDZIENNIK_WEB + ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), + + /* Timetable */ + Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_TIMETABLE, listOf( + ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE to LOGIN_METHOD_EDUDZIENNIK_WEB + ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), + + /* Grades */ + Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_GRADES, listOf( + ENDPOINT_EDUDZIENNIK_WEB_GRADES to LOGIN_METHOD_EDUDZIENNIK_WEB + ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), + + /* Agenda */ + Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_AGENDA, listOf( + ENDPOINT_EDUDZIENNIK_WEB_EXAMS to LOGIN_METHOD_EDUDZIENNIK_WEB, + ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK to LOGIN_METHOD_EDUDZIENNIK_WEB, + ENDPOINT_EDUDZIENNIK_WEB_EVENTS to LOGIN_METHOD_EDUDZIENNIK_WEB + ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), + + /* Homework */ + Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_HOMEWORK, listOf( + ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK to LOGIN_METHOD_EDUDZIENNIK_WEB + ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), + + /* Behaviour */ + Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_BEHAVIOUR, listOf( + ENDPOINT_EDUDZIENNIK_WEB_NOTES to LOGIN_METHOD_EDUDZIENNIK_WEB + ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), + + /* Attendance */ + Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_ATTENDANCE, listOf( + ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE to LOGIN_METHOD_EDUDZIENNIK_WEB + ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), + + /* Announcements */ + Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_ANNOUNCEMENTS, listOf( + ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS to LOGIN_METHOD_EDUDZIENNIK_WEB + ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), + + /* Lucky number */ + Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_LUCKY_NUMBER, listOf( + ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER to LOGIN_METHOD_EDUDZIENNIK_WEB + ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)) +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/EdudziennikData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/EdudziennikData.kt new file mode 100644 index 00000000..fe45c9ef --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/EdudziennikData.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-22 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web.* +import pl.szczodrzynski.edziennik.utils.Utils + +class EdudziennikData(val data: DataEdudziennik, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "EdudziennikData" + } + + init { + nextEndpoint(onSuccess) + } + + private fun nextEndpoint(onSuccess: () -> Unit) { + if (data.targetEndpointIds.isEmpty()) { + onSuccess() + return + } + if (data.cancelled) { + onSuccess() + return + } + useEndpoint(data.targetEndpointIds.removeAt(0)) { + data.progress(data.progressStep) + nextEndpoint(onSuccess) + } + } + + private fun useEndpoint(endpointId: Int, onSuccess: () -> Unit) { + Utils.d(TAG, "Using endpoint $endpointId") + when (endpointId) { + ENDPOINT_EDUDZIENNIK_WEB_START -> { + data.startProgress(R.string.edziennik_progress_endpoint_data) + EdudziennikWebStart(data, onSuccess) + } + ENDPOINT_EDUDZIENNIK_WEB_TEACHERS -> { + data.startProgress(R.string.edziennik_progress_endpoint_teachers) + EdudziennikWebTeachers(data, onSuccess) + } + ENDPOINT_EDUDZIENNIK_WEB_GRADES -> { + data.startProgress(R.string.edziennik_progress_endpoint_grades) + EdudziennikWebGrades(data, onSuccess) + } + ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE -> { + data.startProgress(R.string.edziennik_progress_endpoint_timetable) + EdudziennikWebTimetable(data, onSuccess) + } + ENDPOINT_EDUDZIENNIK_WEB_EXAMS -> { + data.startProgress(R.string.edziennik_progress_endpoint_exams) + EdudziennikWebExams(data, onSuccess) + } + ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE -> { + data.startProgress(R.string.edziennik_progress_endpoint_attendance) + EdudziennikWebAttendance(data, onSuccess) + } + ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS -> { + data.startProgress(R.string.edziennik_progress_endpoint_announcements) + EdudziennikWebAnnouncements(data, onSuccess) + } + ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK -> { + data.startProgress(R.string.edziennik_progress_endpoint_homework) + EdudziennikWebHomework(data, onSuccess) + } + ENDPOINT_EDUDZIENNIK_WEB_EVENTS -> { + data.startProgress(R.string.edziennik_progress_endpoint_events) + EdudziennikWebEvents(data, onSuccess) + } + ENDPOINT_EDUDZIENNIK_WEB_NOTES -> { + data.startProgress(R.string.edziennik_progress_endpoint_notices) + EdudziennikWebNotes(data, onSuccess) + } + ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER -> { + data.startProgress(R.string.edziennik_progress_endpoint_lucky_number) + EdudziennikWebLuckyNumber(data, onSuccess) + } + else -> onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/EdudziennikWeb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/EdudziennikWeb.kt new file mode 100644 index 00000000..2f36a4a8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/EdudziennikWeb.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-22 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data + +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.callback.TextCallbackHandler +import okhttp3.Cookie +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.utils.Utils.d + +open class EdudziennikWeb(open val data: DataEdudziennik) { + companion object { + private const val TAG = "EdudziennikWeb" + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + fun webGet(tag: String, endpoint: String, xhr: Boolean = false, onSuccess: (text: String) -> Unit) { + val url = "https://dziennikel.appspot.com/" + when (endpoint.endsWith('/') || endpoint.contains('?') || endpoint.isEmpty()) { + true -> endpoint + else -> "$endpoint/" + } + + d(tag, "Request: Edudziennik/Web - $url") + + val callback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + if (text == null || response == null) { + data.error(ApiError(tag, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + try { + onSuccess(text) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_EDUDZIENNIK_WEB_REQUEST) + .withThrowable(e) + .withResponse(response) + .withApiResponse(text)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + val error = when (response?.code()) { + 402 -> ERROR_EDUDZIENNIK_WEB_LIMITED_ACCESS + 403 -> ERROR_EDUDZIENNIK_WEB_SESSION_EXPIRED + else -> ERROR_REQUEST_FAILURE + } + data.error(ApiError(tag, error) + .withResponse(response) + .withThrowable(throwable)) + } + } + + data.app.cookieJar.saveFromResponse(null, listOf( + Cookie.Builder() + .name("sessionid") + .value(data.webSessionId!!) + .domain("dziennikel.appspot.com") + .secure().httpOnly().build(), + Cookie.Builder() + .name("semester") + .value((profile?.currentSemester ?: 1).toString()) + .domain("dziennikel.appspot.com") + .secure().httpOnly().build() + )) + + Request.builder() + .url(url) + .userAgent(EDUDZIENNIK_USER_AGENT) + .apply { + if (xhr) header("X-Requested-With", "XMLHttpRequest") + } + .get() + .callback(callback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebAnnouncements.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebAnnouncements.kt new file mode 100644 index 00000000..c0c215c6 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebAnnouncements.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-26 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web + +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.crc32 +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ANNOUNCEMENT_ID +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb +import pl.szczodrzynski.edziennik.data.db.modules.announcements.Announcement +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.splitName +import pl.szczodrzynski.edziennik.utils.models.Date + +class EdudziennikWebAnnouncements(override val data: DataEdudziennik, + val onSuccess: () -> Unit) : EdudziennikWeb(data) { + companion object { + const val TAG = "EdudziennikWebAnnouncements" + } + + init { data.profile?.also { profile -> + webGet(TAG, data.schoolClassEndpoint + "Announcements") { text -> + val doc = Jsoup.parse(text) + + if (doc.getElementsByClass("message").text().trim() != "Brak ogłoszeń.") { + doc.select("table.list tbody tr").forEach { announcementElement -> + val titleElement = announcementElement.child(0).child(0) + + val longId = EDUDZIENNIK_ANNOUNCEMENT_ID.find(titleElement.attr("href"))?.get(1) + ?: return@forEach + val id = longId.crc32() + val subject = titleElement.text() + + val teacherName = announcementElement.child(1).text() + val teacher = teacherName.splitName()?.let { (teacherFirstName, teacherLastName) -> + data.getTeacher(teacherFirstName, teacherLastName) + } ?: return@forEach + + val dateString = announcementElement.getElementsByClass("datetime").first().text() + val startDate = Date.fromY_m_d(dateString) + val addedDate = Date.fromIsoHm(dateString) + + val announcementObject = Announcement( + profileId, + id, + subject, + null, + startDate, + null, + teacher.id, + longId + ) + + data.announcementIgnoreList.add(announcementObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_ANNOUNCEMENT, + id, + profile.empty, + profile.empty, + addedDate + )) + } + } + + data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebAttendance.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebAttendance.kt new file mode 100644 index 00000000..c24fc8d3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebAttendance.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-24 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web + +import pl.szczodrzynski.edziennik.crc32 +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ATTENDANCE_ENTRIES +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ATTENDANCE_TYPE +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ATTENDANCE_TYPES +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.utils.models.Date +import java.util.* + +class EdudziennikWebAttendance(override val data: DataEdudziennik, + val onSuccess: () -> Unit) : EdudziennikWeb(data) { + companion object { + private const val TAG = "EdudziennikWebAttendance" + } + + init { data.profile?.also { profile -> + webGet(TAG, data.studentEndpoint + "Presence") { text -> + + val attendanceTypes = EDUDZIENNIK_ATTENDANCE_TYPES.find(text)?.get(1)?.split(',')?.map { + val type = EDUDZIENNIK_ATTENDANCE_TYPE.find(it.trim()) + val symbol = type?.get(1)?.trim() + val name = type?.get(2)?.trim() + return@map Triple( + symbol, + name, + when (name?.toLowerCase(Locale.ROOT)) { + "obecność" -> Attendance.TYPE_PRESENT + "nieobecność" -> Attendance.TYPE_ABSENT + "spóźnienie" -> Attendance.TYPE_BELATED + "nieobecność usprawiedliwiona" -> Attendance.TYPE_ABSENT_EXCUSED + "dzień wolny" -> Attendance.TYPE_DAY_FREE + "brak zajęć" -> Attendance.TYPE_DAY_FREE + "oddelegowany" -> Attendance.TYPE_RELEASED + else -> Attendance.TYPE_CUSTOM + } + ) + } ?: emptyList() + + EDUDZIENNIK_ATTENDANCE_ENTRIES.findAll(text).forEach { attendanceElement -> + val date = Date.fromY_m_d(attendanceElement[1]) + val lessonNumber = attendanceElement[2].toInt() + val attendanceSymbol = attendanceElement[3] + + val lessons = data.app.db.timetableDao().getForDateNow(profileId, date) + val lesson = lessons.firstOrNull { it.lessonNumber == lessonNumber } + + val id = "${date.stringY_m_d}:$lessonNumber:$attendanceSymbol".crc32() + + val (_, name, type) = attendanceTypes.firstOrNull { (symbol, _, _) -> symbol == attendanceSymbol } + ?: return@forEach + + val startTime = data.lessonRanges.singleOrNull { it.lessonNumber == lessonNumber }?.startTime + ?: return@forEach + + val attendanceObject = Attendance( + profileId, + id, + lesson?.displayTeacherId ?: -1, + lesson?.displaySubjectId ?: -1, + profile.currentSemester, + name, + date, + lesson?.displayStartTime ?: startTime, + type + ) + + data.attendanceList.add(attendanceObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_ATTENDANCE, + id, + profile.empty, + profile.empty, + System.currentTimeMillis() + )) + } + + data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebEvents.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebEvents.kt new file mode 100644 index 00000000..be9574d7 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebEvents.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2020-1-1 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web + +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.crc32 +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_EVENT_ID +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_EVENTS +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.utils.models.Date + +class EdudziennikWebEvents(override val data: DataEdudziennik, + val onSuccess: () -> Unit) : EdudziennikWeb(data) { + companion object { + const val TAG = "EdudziennikWebEvents" + } + + init { data.profile?.also { profile -> + webGet(TAG, data.studentAndClassesEndpoint + "KlassEvent", xhr = true) { text -> + val doc = Jsoup.parseBodyFragment("" + text.trim() + "
    ") + + doc.getElementsByTag("tr").forEach { eventElement -> + val date = Date.fromY_m_d(eventElement.child(1).text()) + + val titleElement = eventElement.child(2).child(0) + val title = titleElement.text().trim() + + val id = EDUDZIENNIK_EVENT_ID.find(titleElement.attr("href"))?.get(1)?.crc32() + ?: return@forEach + + val eventObject = Event( + profileId, + id, + date, + null, + title, + -1, + Event.TYPE_CLASS_EVENT, + false, + -1, + -1, + data.teamClass?.id ?: -1 + ) + + data.eventList.add(eventObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_EVENT, + id, + profile.empty, + profile.empty, + System.currentTimeMillis() + )) + } + + data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_CLASS_EVENT)) + + data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_EVENTS, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebExams.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebExams.kt new file mode 100644 index 00000000..805bed1c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebExams.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-24 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web + +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.crc32 +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_EVENT_TYPE_ID +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_EXAM_ID +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECT_ID +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_EXAMS +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.utils.models.Date + +class EdudziennikWebExams(override val data: DataEdudziennik, + val onSuccess: () -> Unit) : EdudziennikWeb(data) { + companion object { + const val TAG = "EdudziennikWebExams" + } + + init { profile?.also { profile -> + webGet(TAG, data.studentAndClassEndpoint + "Evaluations", xhr = true) { text -> + val doc = Jsoup.parseBodyFragment("" + text.trim() + "
    ") + + doc.select("tr").forEach { examElement -> + val id = EDUDZIENNIK_EXAM_ID.find(examElement.child(0).child(0).attr("href")) + ?.get(1)?.crc32() ?: return@forEach + val topic = examElement.child(0).text().trim() + + val subjectElement = examElement.child(1).child(0) + val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1) + ?: return@forEach + val subjectName = subjectElement.text().trim() + val subject = data.getSubject(subjectId, subjectName) + + val dateString = examElement.child(2).text().trim() + if (dateString.isBlank()) return@forEach + val date = Date.fromY_m_d(dateString) + + val lessons = data.app.db.timetableDao().getForDateNow(profileId, date) + val startTime = lessons.firstOrNull { it.displaySubjectId == subject.id }?.displayStartTime + + val eventTypeElement = examElement.child(3).child(0) + val eventTypeId = EDUDZIENNIK_EVENT_TYPE_ID.find(eventTypeElement.attr("href"))?.get(1) + ?: return@forEach + val eventTypeName = eventTypeElement.text() + val eventType = data.getEventType(eventTypeId, eventTypeName) + + val eventObject = Event( + profileId, + id, + date, + startTime, + topic, + -1, + eventType.id.toInt(), + false, + -1, + subject.id, + data.teamClass?.id ?: -1 + ) + + data.eventList.add(eventObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_EVENT, + id, + profile.empty, + profile.empty, + System.currentTimeMillis() + )) + } + + data.toRemove.add(DataRemoveModel.Events.futureExceptTypes(listOf( + Event.TYPE_HOMEWORK, + Event.TYPE_CLASS_EVENT + ))) + + data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_EXAMS, SYNC_ALWAYS) + onSuccess() + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetAnnouncement.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetAnnouncement.kt new file mode 100644 index 00000000..ddc06669 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetAnnouncement.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-26 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb +import pl.szczodrzynski.edziennik.data.api.events.AnnouncementGetEvent +import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull +import pl.szczodrzynski.edziennik.get + +class EdudziennikWebGetAnnouncement( + override val data: DataEdudziennik, + private val announcement: AnnouncementFull, + val onSuccess: () -> Unit +) : EdudziennikWeb(data) { + companion object { + const val TAG = "EdudziennikWebGetAnnouncement" + } + + init { + webGet(TAG, "Announcement/${announcement.idString}") { text -> + val description = Regexes.EDUDZIENNIK_ANNOUNCEMENT_DESCRIPTION.find(text)?.get(1)?.trim() ?: "" + + announcement.text = description + + EventBus.getDefault().postSticky(AnnouncementGetEvent(announcement)) + + data.announcementList.add(announcement) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGrades.kt new file mode 100644 index 00000000..d3c512a2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGrades.kt @@ -0,0 +1,212 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-25 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web + +import android.graphics.Color +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.colorFromCssName +import pl.szczodrzynski.edziennik.crc32 +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.* +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.models.Date + +class EdudziennikWebGrades(override val data: DataEdudziennik, + val onSuccess: () -> Unit) : EdudziennikWeb(data) { + companion object { + private const val TAG = "EdudziennikWebGrades" + } + + init { data.profile?.also { profile -> + webGet(TAG, data.studentEndpoint + "start") { text -> + val doc = Jsoup.parse(text) + + val subjects = doc.select("#student_grades tbody").firstOrNull()?.children() + + subjects?.forEach { subjectElement -> + if (subjectElement.id().isBlank()) return@forEach + + val subjectId = subjectElement.id().trim() + val subjectName = subjectElement.child(0).text().trim() + val subject = data.getSubject(subjectId, subjectName) + + val gradeType = when { + subjectElement.select("#sum").text().isNotBlank() -> TYPE_POINT_SUM + else -> TYPE_NORMAL + } + + val gradeCountToAverage = subjectElement.select("#avg").text().isNotBlank() + + val grades = subjectElement.select(".grade[data-edited]") + val gradesInfo = subjectElement.select(".grade-tip") + + val gradeValues = if (grades.isNotEmpty()) { + subjects.select(".avg-$subjectId .grade-tip > p").first() + .text().split('+').map { + val split = it.split('*') + val weight = split[0].trim().toFloat() + val value = split[1].trim().toFloat() + + Pair(value, weight) + } + } else emptyList() + + grades.forEachIndexed { index, gradeElement -> + val id = Regexes.EDUDZIENNIK_GRADE_ID.find(gradeElement.attr("href"))?.get(1)?.crc32() + ?: return@forEachIndexed + val (value, weight) = gradeValues[index] + val name = gradeElement.text().trim().let { + if (it.contains(',') || it.contains('.')) { + val replaced = it.replace(',', '.') + val float = replaced.toFloatOrNull() + + if (float != null && float % 1 == 0f) float.toInt().toString() + else it + } else it + } + + val info = gradesInfo[index] + val fullName = info.child(0).text().trim() + val columnName = info.child(4).text().trim() + val comment = info.ownText() + + val description = columnName + if (comment.isNotBlank()) " - $comment" else "" + + val (teacherLastName, teacherFirstName) = info.child(1).text().split(' ') + val teacher = data.getTeacher(teacherFirstName, teacherLastName) + + val addedDate = info.child(2).text().split(' ').let { + val day = it[0].toInt() + val month = Utils.monthFromName(it[1]) + val year = it[2].toInt() + + Date(year, month, day).inMillis + } + + val color = Regexes.STYLE_CSS_COLOR.find(gradeElement.attr("style"))?.get(1)?.let { + if (it.startsWith('#')) Color.parseColor(it) + else colorFromCssName(it) + } ?: -1 + + val gradeObject = Grade( + profileId, + id, + fullName, + color, + description, + name, + value, + if (gradeCountToAverage) weight else 0f, + profile.currentSemester, + teacher.id, + subject.id + ).apply { + type = gradeType + } + + data.gradeList.add(gradeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_GRADE, + id, + profile.empty, + profile.empty, + addedDate + )) + } + + val proposed = subjectElement.select(".proposal").firstOrNull()?.text()?.trim() + + if (proposed != null && proposed.isNotBlank()) { + val proposedGradeObject = Grade( + profileId, + (-1 * subject.id) - 1, + "", + -1, + "", + proposed, + proposed.toFloatOrNull() ?: 0f, + 0f, + profile.currentSemester, + -1, + subject.id + ).apply { + type = when (semester) { + 1 -> TYPE_SEMESTER1_PROPOSED + else -> TYPE_SEMESTER2_PROPOSED + } + } + + data.gradeList.add(proposedGradeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_GRADE, + proposedGradeObject.id, + profile.empty, + profile.empty, + System.currentTimeMillis() + )) + } + + val final = subjectElement.select(".final").firstOrNull()?.text()?.trim() + + if (final != null && final.isNotBlank()) { + val finalGradeObject = Grade( + profileId, + (-1 * subject.id) - 2, + "", + -1, + "", + final, + final.toFloatOrNull() ?: 0f, + 0f, + profile.currentSemester, + -1, + subject.id + ).apply { + type = when (semester) { + 1 -> TYPE_SEMESTER1_FINAL + else -> TYPE_SEMESTER2_FINAL + } + } + + data.gradeList.add(finalGradeObject) + data.metadataList.add(Metadata( + data.profileId, + Metadata.TYPE_GRADE, + finalGradeObject.id, + profile.empty, + profile.empty, + System.currentTimeMillis() + )) + } + } + + if (!subjects.isNullOrEmpty()) { + data.toRemove.addAll(listOf( + TYPE_NORMAL, + TYPE_POINT_SUM, + TYPE_SEMESTER1_PROPOSED, + TYPE_SEMESTER2_PROPOSED, + TYPE_SEMESTER1_FINAL, + TYPE_SEMESTER2_FINAL + ).map { + DataRemoveModel.Grades.semesterWithType(profile.currentSemester, it) + }) + } + + data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_GRADES, SYNC_ALWAYS) + onSuccess() + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebHomework.kt new file mode 100644 index 00000000..af846615 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebHomework.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-29 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web + +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.crc32 +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_HOMEWORK_ID +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECT_ID +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.splitName +import pl.szczodrzynski.edziennik.utils.models.Date + +class EdudziennikWebHomework(override val data: DataEdudziennik, + val onSuccess: () -> Unit) : EdudziennikWeb(data) { + companion object { + const val TAG = "EdudziennikWebHomework" + } + + init { data.profile?.also { profile -> + webGet(TAG, data.courseStudentEndpoint + "Homework", xhr = true) { text -> + val doc = Jsoup.parseBodyFragment("" + text.trim() + "
    ") + + if (doc.getElementsByClass("message").text().trim() != "Brak prac domowych") { + doc.getElementsByTag("tr").forEach { homeworkElement -> + val dateElement = homeworkElement.getElementsByClass("date").first().child(0) + val id = EDUDZIENNIK_HOMEWORK_ID.find(dateElement.attr("href"))?.get(1)?.crc32() + ?: return@forEach + val date = Date.fromY_m_d(dateElement.text()) + + val subjectElement = homeworkElement.child(1).child(0) + val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1) + ?: return@forEach + val subjectName = subjectElement.text() + val subject = data.getSubject(subjectId, subjectName) + + val lessons = data.app.db.timetableDao().getForDateNow(profileId, date) + val startTime = lessons.firstOrNull { it.subjectId == subject.id }?.displayStartTime + + val teacherName = homeworkElement.child(2).text() + val teacher = teacherName.splitName()?.let { (teacherFirstName, teacherLastName) -> + data.getTeacher(teacherFirstName, teacherLastName) + } ?: return@forEach + + val topic = homeworkElement.child(4).text() + + val eventObject = Event( + profileId, + id, + date, + startTime, + topic, + -1, + Event.TYPE_HOMEWORK, + false, + teacher.id, + subject.id, + data.teamClass?.id ?: -1 + ) + + data.eventList.add(eventObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_HOMEWORK, + id, + profile.empty, + profile.empty, + System.currentTimeMillis() + )) + } + } + + data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_HOMEWORK)) + + data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebLuckyNumber.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebLuckyNumber.kt new file mode 100644 index 00000000..45dfe8fb --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebLuckyNumber.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-23 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web + +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.luckynumber.LuckyNumber +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date + +class EdudziennikWebLuckyNumber(override val data: DataEdudziennik, + val onSuccess: () -> Unit) : EdudziennikWeb(data) { + companion object { + private const val TAG = "EdudziennikWebLuckyNumber" + } + + init { data.profile?.also { profile -> + webGet(TAG, data.schoolEndpoint + "Lucky", xhr = true) { text -> + text.toIntOrNull()?.also { luckyNumber -> + val luckyNumberObject = LuckyNumber( + profileId, + Date.getToday(), + luckyNumber + ) + + data.luckyNumberList.add(luckyNumberObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_LUCKY_NUMBER, + luckyNumberObject.date.value.toLong(), + profile.empty, + profile.empty, + System.currentTimeMillis() + )) + } + + data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebNotes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebNotes.kt new file mode 100644 index 00000000..213dd2d2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebNotes.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2020-1-1 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web + +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.crc32 +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_NOTE_ID +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_NOTES +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.notices.Notice +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.utils.models.Date + +class EdudziennikWebNotes(override val data: DataEdudziennik, + val onSuccess: () -> Unit) : EdudziennikWeb(data) { + companion object { + const val TAG = "EdudziennikWebNotes" + } + + init { data.profile?.also { profile -> + webGet(TAG, data.classStudentEndpoint + "RegistryNotesStudent", xhr = true) { text -> + val doc = Jsoup.parseBodyFragment("" + text.trim() + "
    ") + + doc.getElementsByTag("tr").forEach { noteElement -> + val dateElement = noteElement.getElementsByClass("date").first().child(0) + val addedDate = Date.fromY_m_d(dateElement.text()).inMillis + + val id = EDUDZIENNIK_NOTE_ID.find(dateElement.attr("href"))?.get(0)?.crc32() + ?: return@forEach + + val teacherName = noteElement.child(1).text() + val teacher = data.getTeacherByFirstLast(teacherName) + + val description = noteElement.child(3).text() + + val noticeObject = Notice( + profileId, + id, + description, + profile.currentSemester, + Notice.TYPE_NEUTRAL, + teacher.id + ) + + data.noticeList.add(noticeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_NOTICE, + id, + profile.empty, + profile.empty, + addedDate + )) + } + + data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_NOTES, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebStart.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebStart.kt new file mode 100644 index 00000000..e21d85da --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebStart.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-23 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web + +import pl.szczodrzynski.edziennik.MONTH +import pl.szczodrzynski.edziennik.crc32 +import pl.szczodrzynski.edziennik.data.api.ERROR_EDUDZIENNIK_WEB_TEAM_MISSING +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECTS_START +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_START +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.teams.Team +import pl.szczodrzynski.edziennik.firstLettersName +import pl.szczodrzynski.edziennik.get + +class EdudziennikWebStart(override val data: DataEdudziennik, + val onSuccess: () -> Unit) : EdudziennikWeb(data) { + companion object { + private const val TAG = "EdudziennikWebStart" + } + + init { + webGet(TAG, data.studentEndpoint + "start") { text -> + getSchoolAndTeam(text) + getSubjects(text) + + data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_START, MONTH) + onSuccess() + } + } + + private fun getSchoolAndTeam(text: String) { + val schoolId = Regexes.EDUDZIENNIK_SCHOOL_DETAIL_ID.find(text)?.get(1)?.trim() + val schoolLongName = Regexes.EDUDZIENNIK_SCHOOL_DETAIL_NAME.find(text)?.get(1)?.trim() + data.schoolId = schoolId + + val classId = Regexes.EDUDZIENNIK_CLASS_DETAIL_ID.find(text)?.get(1)?.trim() + val className = Regexes.EDUDZIENNIK_CLASS_DETAIL_NAME.find(text)?.get(1)?.trim() + data.classId = classId + + if (classId == null || className == null || schoolId == null || schoolLongName == null) { + data.error(ApiError(TAG, ERROR_EDUDZIENNIK_WEB_TEAM_MISSING) + .withApiResponse(text)) + return + } + + val schoolName = schoolId.crc32().toString() + schoolLongName.firstLettersName + "_edu" + data.schoolName = schoolName + + val teamId = classId.crc32() + val teamCode = "$schoolName:$className" + + val teamObject = Team( + data.profileId, + teamId, + className, + Team.TYPE_CLASS, + teamCode, + -1 + ) + + data.teamClass = teamObject + data.teamList.put(teamObject.id, teamObject) + } + + private fun getSubjects(text: String) { + EDUDZIENNIK_SUBJECTS_START.findAll(text).forEach { + val id = it[1].trim() + val name = it[2].trim() + data.getSubject(id, name) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTeachers.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTeachers.kt new file mode 100644 index 00000000..534e91cf --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTeachers.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-25 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web + +import pl.szczodrzynski.edziennik.MONTH +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_TEACHERS +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_TEACHERS +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb +import pl.szczodrzynski.edziennik.get + +class EdudziennikWebTeachers(override val data: DataEdudziennik, + val onSuccess: () -> Unit) : EdudziennikWeb(data) { + companion object { + private const val TAG = "EdudziennikWebTeachers" + } + + init { + webGet(TAG, data.studentAndTeacherClassEndpoint + "grid") { text -> + EDUDZIENNIK_TEACHERS.findAll(text).forEach { + val lastName = it[1].trim() + val firstName = it[2].trim() + data.getTeacher(firstName, lastName) + } + + data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_TEACHERS, MONTH) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTimetable.kt new file mode 100644 index 00000000..1c90578f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTimetable.kt @@ -0,0 +1,164 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-23 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web + +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.data.api.ERROR_EDUDZIENNIK_WEB_TIMETABLE_NOT_PUBLIC +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECT_ID +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_TEACHER_ID +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonRange +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.splitName +import pl.szczodrzynski.edziennik.utils.Utils.d +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.edziennik.utils.models.Week + +class EdudziennikWebTimetable(override val data: DataEdudziennik, + val onSuccess: () -> Unit) : EdudziennikWeb(data) { + companion object { + private const val TAG = "EdudziennikWebTimetable" + } + + init { data.profile?.also { profile -> + + val currentWeekStart = Week.getWeekStart() + + if (Date.getToday().weekDay > 4) { + currentWeekStart.stepForward(0, 0, 7) + } + + val getDate = data.arguments?.getString("weekStart") ?: currentWeekStart.stringY_m_d + + val weekStart = Date.fromY_m_d(getDate) + val weekEnd = weekStart.clone().stepForward(0, 0, 6) + + webGet(TAG, data.timetableEndpoint + "print?date=$getDate") { text -> + val doc = Jsoup.parse(text) + + val dataDays = mutableListOf() + val dataStart = weekStart.clone() + while (dataStart <= weekEnd) { + dataDays += dataStart.value + dataStart.stepForward(0, 0, 1) + } + + val table = doc.select("#Schedule tbody").first() + + if (table.text().trim() == "Brak planu lekcji.") { + val today = Date.getToday() + val schoolYearStart = if (today.month >= 9) today.year else today.year - 1 + + if (weekStart >= Date(schoolYearStart, 9, 1)) { + data.error(ApiError(TAG, ERROR_EDUDZIENNIK_WEB_TIMETABLE_NOT_PUBLIC) + .withApiResponse(text)) + onSuccess() + return@webGet + } + } else { + table.children().forEach { row -> + val rowElements = row.children() + + val lessonNumber = rowElements[0].text().toInt() + + val times = rowElements[1].text().split('-') + val startTime = Time.fromH_m(times[0].trim()) + val endTime = Time.fromH_m(times[1].trim()) + + data.lessonRanges.singleOrNull { + it.lessonNumber == lessonNumber && it.startTime == startTime && it.endTime == endTime + } ?: run { + data.lessonRanges.put(lessonNumber, LessonRange(profileId, lessonNumber, startTime, endTime)) + } + + rowElements.subList(2, rowElements.size).forEachIndexed { index, lesson -> + val course = lesson.select(".course").firstOrNull() ?: return@forEachIndexed + val info = course.select("span > span") + + if (info.isEmpty()) return@forEachIndexed + + val type = when (course.hasClass("substitute")) { + true -> Lesson.TYPE_CHANGE + else -> Lesson.TYPE_NORMAL + } + + /* Getting subject */ + + val subjectElement = info[0].child(0) + val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1) + ?: return@forEachIndexed + val subjectName = subjectElement.text().trim() + val subject = data.getSubject(subjectId, subjectName) + + /* Getting teacher */ + + val teacherId = if (info.size >= 2) { + val teacherElement = info[1].child(0) + val teacherLongId = EDUDZIENNIK_TEACHER_ID.find(teacherElement.attr("href"))?.get(1) + val teacherName = teacherElement.text().trim() + teacherName.splitName()?.let { (teacherLastName, teacherFirstName) -> + data.getTeacher(teacherFirstName, teacherLastName, teacherLongId) + }?.id ?: -1 + } else -1 + + val lessonObject = Lesson(profileId, -1).also { + it.type = type + it.date = weekStart.clone().stepForward(0, 0, index) + it.lessonNumber = lessonNumber + it.startTime = startTime + it.endTime = endTime + it.subjectId = subject.id + it.teacherId = teacherId + + it.id = it.buildId() + } + + data.lessonNewList.add(lessonObject) + dataDays.remove(lessonObject.date!!.value) + + if (type != Lesson.TYPE_NORMAL) { + val seen = profile.empty || lessonObject.date!! < Date.getToday() + + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_LESSON_CHANGE, + lessonObject.id, + seen, + seen, + System.currentTimeMillis() + )) + } + } + } + } + + for (day in dataDays) { + val lessonDate = Date.fromValue(day) + data.lessonNewList += Lesson(profileId, lessonDate.value.toLong()).apply { + type = Lesson.TYPE_NO_LESSONS + date = lessonDate + } + } + + d(TAG, "Clearing lessons between ${weekStart.stringY_m_d} and ${weekEnd.stringY_m_d} - timetable downloaded for $getDate") + + if (data.timetableNotPublic) data.timetableNotPublic = false + + data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd)) + data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/firstlogin/EdudziennikFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/firstlogin/EdudziennikFirstLogin.kt new file mode 100644 index 00000000..e9b9156c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/firstlogin/EdudziennikFirstLogin.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-22 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.firstlogin + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ACCOUNT_NAME_START +import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_STUDENTS_START +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.EdudziennikLoginWeb +import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.fixName +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.getShortName +import pl.szczodrzynski.edziennik.utils.Utils + +class EdudziennikFirstLogin(val data: DataEdudziennik, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "EdudziennikFirstLogin" + } + + private val web = EdudziennikWeb(data) + private val profileList = mutableListOf() + + init { + EdudziennikLoginWeb(data) { + web.webGet(TAG, "") { text -> + val accountName = EDUDZIENNIK_ACCOUNT_NAME_START.find(text)?.get(1)?.fixName() + + EDUDZIENNIK_STUDENTS_START.findAll(text).forEach { + val studentId = it[1] + val studentName = it[2].fixName() + + if (studentId.isBlank() || studentName.isBlank()) return@forEach + + val profile = Profile() + profile.studentNameLong = studentName + profile.studentNameShort = studentName.getShortName() + profile.accountNameLong = if (studentName == accountName) null else accountName + profile.studentSchoolYear = Utils.getCurrentSchoolYear() + profile.name = studentName + profile.subname = data.loginEmail + profile.empty = true + profile.putStudentData("studentId", studentId) + profileList.add(profile) + } + + EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore)) + onSuccess() + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/login/EdudziennikLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/login/EdudziennikLogin.kt new file mode 100644 index 00000000..1cb2fd07 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/login/EdudziennikLogin.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-22 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_EDUDZIENNIK_WEB +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.utils.Utils + +class EdudziennikLogin(val data: DataEdudziennik, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "EdudziennikLogin" + } + + private var cancelled = false + + init { + nextLoginMethod(onSuccess) + } + + private fun nextLoginMethod(onSuccess: () -> Unit) { + if (data.targetLoginMethodIds.isEmpty()) { + onSuccess() + return + } + if (cancelled) { + onSuccess() + return + } + useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId -> + data.progress(data.progressStep) + if (usedMethodId != -1) + data.loginMethods.add(usedMethodId) + nextLoginMethod(onSuccess) + } + } + + private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) { + // this should never be true + if (data.loginMethods.contains(loginMethodId)) { + onSuccess(-1) + return + } + Utils.d(TAG, "Using login method $loginMethodId") + when (loginMethodId) { + LOGIN_METHOD_EDUDZIENNIK_WEB -> { + data.startProgress(R.string.edziennik_progress_login_edudziennik_web) + EdudziennikLoginWeb(data) { onSuccess(loginMethodId) } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/login/EdudziennikLoginWeb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/login/EdudziennikLoginWeb.kt new file mode 100644 index 00000000..8387a775 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/login/EdudziennikLoginWeb.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-22 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login + +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.callback.TextCallbackHandler +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.getUnixDate +import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.utils.Utils.d + +class EdudziennikLoginWeb(val data: DataEdudziennik, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "EdudziennikLoginWeb" + } + + init { run { + if (data.isWebLoginValid()) { + onSuccess() + } + else { + data.app.cookieJar.clearForDomain("dziennikel.appspot.com") + if (data.loginEmail.isNotNullNorEmpty() && data.loginPassword.isNotNullNorEmpty()) { + loginWithCredentials() + } + else { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + } + } + }} + + private fun loginWithCredentials() { + d(TAG, "Request: Edudziennik/Login/Web - https://dziennikel.appspot.com/login/?next=/") + + val callback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + if (text == null || response == null) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + val url = response.raw().request().url().toString() + + if (!url.contains("Student")) { + when { + text.contains("Wprowadzono nieprawidłową nazwę użytkownika lub hasło.") -> ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN + else -> ERROR_LOGIN_EDUDZIENNIK_WEB_OTHER + }.let { errorCode -> + data.error(ApiError(TAG, errorCode) + .withApiResponse(text) + .withResponse(response)) + return + } + } + + val cookies = data.app.cookieJar.getForDomain("dziennikel.appspot.com") + val sessionId = cookies.firstOrNull { it.name() == "sessionid" }?.value() + + if (sessionId == null) { + data.error(ApiError(TAG, ERROR_LOGIN_EDUDZIENNIK_WEB_NO_SESSION_ID) + .withResponse(response) + .withApiResponse(text)) + return + } + + data.webSessionId = sessionId + data.webSessionIdExpiryTime = response.getUnixDate() + 45 * 60 /* 45 min */ + onSuccess() + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + Request.builder() + .url("https://dziennikel.appspot.com/login/?next=/") + .userAgent(EDUDZIENNIK_USER_AGENT) + .contentType("application/x-www-form-urlencoded") + .addParameter("email", data.loginEmail) + .addParameter("password", data.loginPassword) + .addParameter("auth_method", "password") + .addParameter("next", "/") + .post() + .callback(callback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/DataIdziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/DataIdziennik.kt new file mode 100644 index 00000000..c3bd925a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/DataIdziennik.kt @@ -0,0 +1,178 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-25. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik + +import androidx.core.util.set +import okhttp3.Cookie +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_IDZIENNIK_API +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_IDZIENNIK_WEB +import pl.szczodrzynski.edziennik.data.api.models.Data +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher + +class DataIdziennik(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { + + fun isWebLoginValid() = loginExpiryTime-30 > currentTimeUnix() && webSessionId.isNotNullNorEmpty() && webAuth.isNotNullNorEmpty() + fun isApiLoginValid() = apiExpiryTime-30 > currentTimeUnix() && apiBearer.isNotNullNorEmpty() + + override fun satisfyLoginMethods() { + loginMethods.clear() + if (isWebLoginValid()) { + loginMethods += LOGIN_METHOD_IDZIENNIK_WEB + app.cookieJar.saveFromResponse(null, listOf( + Cookie.Builder() + .name("ASP.NET_SessionId_iDziennik") + .value(webSessionId!!) + .domain("iuczniowie.progman.pl") + .secure().httpOnly().build(), + Cookie.Builder() + .name(".ASPXAUTH") + .value(webAuth!!) + .domain("iuczniowie.progman.pl") + .secure().httpOnly().build() + )) + } + if (isApiLoginValid()) + loginMethods += LOGIN_METHOD_IDZIENNIK_API + } + + private var mLoginExpiryTime: Long? = null + var loginExpiryTime: Long + get() { mLoginExpiryTime = mLoginExpiryTime ?: loginStore.getLoginData("loginExpiryTime", 0L); return mLoginExpiryTime ?: 0L } + set(value) { loginStore.putLoginData("loginExpiryTime", value); mLoginExpiryTime = value } + + private var mApiExpiryTime: Long? = null + var apiExpiryTime: Long + get() { mApiExpiryTime = mApiExpiryTime ?: loginStore.getLoginData("apiExpiryTime", 0L); return mApiExpiryTime ?: 0L } + set(value) { loginStore.putLoginData("apiExpiryTime", value); mApiExpiryTime = value } + + /* __ __ _ + \ \ / / | | + \ \ /\ / /__| |__ + \ \/ \/ / _ \ '_ \ + \ /\ / __/ |_) | + \/ \/ \___|_._*/ + private var mWebSchoolName: String? = null + var webSchoolName: String? + get() { mWebSchoolName = mWebSchoolName ?: loginStore.getLoginData("schoolName", null); return mWebSchoolName } + set(value) { loginStore.putLoginData("schoolName", value); mWebSchoolName = value } + private var mWebUsername: String? = null + var webUsername: String? + get() { mWebUsername = mWebUsername ?: loginStore.getLoginData("username", null); return mWebUsername } + set(value) { loginStore.putLoginData("username", value); mWebUsername = value } + private var mWebPassword: String? = null + var webPassword: String? + get() { mWebPassword = mWebPassword ?: loginStore.getLoginData("password", null); return mWebPassword } + set(value) { loginStore.putLoginData("password", value); mWebPassword = value } + + private var mWebSessionId: String? = null + var webSessionId: String? + get() { mWebSessionId = mWebSessionId ?: loginStore.getLoginData("webSessionId", null); return mWebSessionId } + set(value) { loginStore.putLoginData("webSessionId", value); mWebSessionId = value } + private var mWebAuth: String? = null + var webAuth: String? + get() { mWebAuth = mWebAuth ?: loginStore.getLoginData("webAuth", null); return mWebAuth } + set(value) { loginStore.putLoginData("webAuth", value); mWebAuth = value } + + /* _ + /\ (_) + / \ _ __ _ + / /\ \ | '_ \| | + / ____ \| |_) | | + /_/ \_\ .__/|_| + | | + |*/ + private var mApiBearer: String? = null + var apiBearer: String? + get() { mApiBearer = mApiBearer ?: loginStore.getLoginData("apiBearer", null); return mApiBearer } + set(value) { loginStore.putLoginData("apiBearer", value); mApiBearer = value } + + /* ____ _ _ + / __ \| | | | + | | | | |_| |__ ___ _ __ + | | | | __| '_ \ / _ \ '__| + | |__| | |_| | | | __/ | + \____/ \__|_| |_|\___|*/ + private var mStudentId: String? = null + var studentId: String? + get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", null); return mStudentId } + set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } + + private var mRegisterId: Int? = null + var registerId: Int + get() { mRegisterId = mRegisterId ?: profile?.getStudentData("registerId", 0); return mRegisterId ?: 0 } + set(value) { profile?.putStudentData("registerId", value) ?: return; mRegisterId = value } + + private var mSchoolYearId: Int? = null + var schoolYearId: Int + get() { mSchoolYearId = mSchoolYearId ?: profile?.getStudentData("schoolYearId", 0); return mSchoolYearId ?: 0 } + set(value) { profile?.putStudentData("schoolYearId", value) ?: return; mSchoolYearId = value } + + + + /* _ _ _ _ _ + | | | | | (_) | + | | | | |_ _| |___ + | | | | __| | / __| + | |__| | |_| | \__ \ + \____/ \__|_|_|__*/ + fun getSubject(name: String, id: Long?, shortName: String): Subject { + var subject = if (id == null) + subjectList.singleOrNull { it.longName == name } + else + subjectList.singleOrNull { it.id == id } + + if (subject == null) { + subject = Subject(profileId, id ?: name.crc16().toLong(), name, shortName) + subjectList[subject.id] = subject + } + return subject + } + + fun getTeacher(firstName: String, lastName: String): Teacher { + val teacher = teacherList.singleOrNull { it.fullName == "$firstName $lastName" } + return validateTeacher(teacher, firstName, lastName) + } + + fun getTeacher(firstNameChar: Char, lastName: String): Teacher { + val teacher = teacherList.singleOrNull { it.shortName == "$firstNameChar.$lastName" } + return validateTeacher(teacher, firstNameChar.toString(), lastName) + } + + fun getTeacherByLastFirst(nameLastFirst: String): Teacher { + val nameParts = nameLastFirst.split(" ") + return if (nameParts.size == 1) getTeacher(nameParts[0], "") else getTeacher(nameParts[1], nameParts[0]) + } + + fun getTeacherByFirstLast(nameFirstLast: String): Teacher { + val nameParts = nameFirstLast.split(" ") + return if (nameParts.size == 1) getTeacher(nameParts[0], "") else getTeacher(nameParts[0], nameParts[1]) + } + + fun getTeacherByFDotLast(nameFDotLast: String): Teacher { + val nameParts = nameFDotLast.split(".") + return if (nameParts.size == 1) getTeacher(nameParts[0], "") else getTeacher(nameParts[0][0], nameParts[1]) + } + + fun getTeacherByFDotSpaceLast(nameFDotSpaceLast: String): Teacher { + val nameParts = nameFDotSpaceLast.split(".") + return if (nameParts.size == 1) getTeacher(nameParts[0], "") else getTeacher(nameParts[0][0], nameParts[1]) + } + + private fun validateTeacher(teacher: Teacher?, firstName: String, lastName: String): Teacher { + (teacher ?: Teacher(profileId, -1, firstName, lastName).apply { + id = shortName.crc16().toLong() + teacherList[id] = this + }).apply { + if (firstName.length > 1) + name = firstName + surname = lastName + return this + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/Idziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/Idziennik.kt new file mode 100644 index 00000000..b2b0b2e2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/Idziennik.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-25. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikData +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web.IdziennikWebGetAttachment +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web.IdziennikWebGetMessage +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web.IdziennikWebGetRecipientList +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web.IdziennikWebSendMessage +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.firstlogin.IdziennikFirstLogin +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.login.IdziennikLogin +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.utils.Utils.d + +class Idziennik(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface { + companion object { + private const val TAG = "Idziennik" + } + + val internalErrorList = mutableListOf() + val data: DataIdziennik + private var afterLogin: (() -> Unit)? = null + + init { + data = DataIdziennik(app, profile, loginStore).apply { + callback = wrapCallback(this@Idziennik.callback) + satisfyLoginMethods() + } + } + + private fun completed() { + data.saveData() + data.notify { + callback.onCompleted() + } + } + + /* _______ _ _ _ _ _ + |__ __| | /\ | | (_) | | | + | | | |__ ___ / \ | | __ _ ___ _ __ _| |_| |__ _ __ ___ + | | | '_ \ / _ \ / /\ \ | |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \ + | | | | | | __/ / ____ \| | (_| | (_) | | | | |_| | | | | | | | | + |_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| + __/ | + |__*/ + override fun sync(featureIds: List, viewId: Int?, arguments: JsonObject?) { + data.arguments = arguments + data.prepare(idziennikLoginMethods, IdziennikFeatures, featureIds, viewId) + login() + } + + private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) { + d(TAG, "Trying to login with ${data.targetLoginMethodIds}") + if (internalErrorList.isNotEmpty()) { + d(TAG, " - Internal errors:") + internalErrorList.forEach { d(TAG, " - code $it") } + } + loginMethodId?.let { data.prepareFor(idziennikLoginMethods, it) } + afterLogin?.let { this.afterLogin = it } + IdziennikLogin(data) { + data() + } + } + + private fun data() { + d(TAG, "Endpoint IDs: ${data.targetEndpointIds}") + if (internalErrorList.isNotEmpty()) { + d(TAG, " - Internal errors:") + internalErrorList.forEach { d(TAG, " - code $it") } + } + afterLogin?.invoke() ?: IdziennikData(data) { + completed() + } + } + + override fun getMessage(message: MessageFull) { + login(LOGIN_METHOD_IDZIENNIK_WEB) { + IdziennikWebGetMessage(data, message) { + completed() + } + } + } + + override fun sendMessage(recipients: List, subject: String, text: String) { + login(LOGIN_METHOD_IDZIENNIK_API) { + IdziennikWebSendMessage(data, recipients, subject, text) { + completed() + } + } + } + + override fun markAllAnnouncementsAsRead() {} + override fun getAnnouncement(announcement: AnnouncementFull) {} + + override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) { + login(LOGIN_METHOD_IDZIENNIK_WEB) { + IdziennikWebGetAttachment(data, message, attachmentId, attachmentName) { + completed() + } + } + } + + override fun getRecipientList() { + login(LOGIN_METHOD_IDZIENNIK_WEB) { + IdziennikWebGetRecipientList(data) { + completed() + } + } + } + + override fun firstLogin() { IdziennikFirstLogin(data) { completed() } } + override fun cancel() { + d(TAG, "Cancelled") + data.cancel() + } + + private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { + return object : EdziennikCallback { + override fun onCompleted() { callback.onCompleted() } + override fun onProgress(step: Float) { callback.onProgress(step) } + override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) } + override fun onError(apiError: ApiError) { + if (apiError.errorCode in internalErrorList) { + // finish immediately if the same error occurs twice during the same sync + callback.onError(apiError) + return + } + internalErrorList.add(apiError.errorCode) + when (apiError.errorCode) { + ERROR_LOGIN_IDZIENNIK_WEB_NO_SESSION, + ERROR_LOGIN_IDZIENNIK_WEB_NO_AUTH, + ERROR_LOGIN_IDZIENNIK_WEB_NO_BEARER, + ERROR_IDZIENNIK_WEB_ACCESS_DENIED, + ERROR_IDZIENNIK_API_ACCESS_DENIED -> { + data.loginMethods.remove(LOGIN_METHOD_IDZIENNIK_WEB) + data.prepareFor(idziennikLoginMethods, LOGIN_METHOD_IDZIENNIK_WEB) + data.loginExpiryTime = 0 + login() + } + ERROR_IDZIENNIK_API_NO_REGISTER -> { + data() + } + else -> callback.onError(apiError) + } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/IdziennikFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/IdziennikFeatures.kt new file mode 100644 index 00000000..02aaabb2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/IdziennikFeatures.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-25. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik + +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.models.Feature + +const val ENDPOINT_IDZIENNIK_WEB_TIMETABLE = 1030 +const val ENDPOINT_IDZIENNIK_WEB_GRADES = 1040 +const val ENDPOINT_IDZIENNIK_WEB_PROPOSED_GRADES = 1050 +const val ENDPOINT_IDZIENNIK_WEB_EXAMS = 1060 +const val ENDPOINT_IDZIENNIK_WEB_HOMEWORK = 1061 +const val ENDPOINT_IDZIENNIK_WEB_NOTICES = 1070 +const val ENDPOINT_IDZIENNIK_WEB_ANNOUNCEMENTS = 1080 +const val ENDPOINT_IDZIENNIK_WEB_ATTENDANCE = 1090 +const val ENDPOINT_IDZIENNIK_WEB_MESSAGES_INBOX = 1110 +const val ENDPOINT_IDZIENNIK_WEB_MESSAGES_SENT = 1120 +const val ENDPOINT_IDZIENNIK_API_CURRENT_REGISTER = 2010 +const val ENDPOINT_IDZIENNIK_API_MESSAGES_INBOX = 2110 +const val ENDPOINT_IDZIENNIK_API_MESSAGES_SENT = 2120 + +val IdziennikFeatures = listOf( + Feature(LOGIN_TYPE_IDZIENNIK, FEATURE_TIMETABLE, listOf( + ENDPOINT_IDZIENNIK_WEB_TIMETABLE to LOGIN_METHOD_IDZIENNIK_WEB + ), listOf(LOGIN_METHOD_IDZIENNIK_WEB)), + + Feature(LOGIN_TYPE_IDZIENNIK, FEATURE_GRADES, listOf( + ENDPOINT_IDZIENNIK_WEB_GRADES to LOGIN_METHOD_IDZIENNIK_WEB, + ENDPOINT_IDZIENNIK_WEB_PROPOSED_GRADES to LOGIN_METHOD_IDZIENNIK_WEB + ), listOf(LOGIN_METHOD_IDZIENNIK_WEB)), + + Feature(LOGIN_TYPE_IDZIENNIK, FEATURE_AGENDA, listOf( + ENDPOINT_IDZIENNIK_WEB_EXAMS to LOGIN_METHOD_IDZIENNIK_WEB + ), listOf(LOGIN_METHOD_IDZIENNIK_WEB)), + + Feature(LOGIN_TYPE_IDZIENNIK, FEATURE_HOMEWORK, listOf( + ENDPOINT_IDZIENNIK_WEB_HOMEWORK to LOGIN_METHOD_IDZIENNIK_WEB + ), listOf(LOGIN_METHOD_IDZIENNIK_WEB)), + + Feature(LOGIN_TYPE_IDZIENNIK, FEATURE_BEHAVIOUR, listOf( + ENDPOINT_IDZIENNIK_WEB_NOTICES to LOGIN_METHOD_IDZIENNIK_WEB + ), listOf(LOGIN_METHOD_IDZIENNIK_WEB)), + + Feature(LOGIN_TYPE_IDZIENNIK, FEATURE_ATTENDANCE, listOf( + ENDPOINT_IDZIENNIK_WEB_ATTENDANCE to LOGIN_METHOD_IDZIENNIK_WEB + ), listOf(LOGIN_METHOD_IDZIENNIK_WEB)), + + Feature(LOGIN_TYPE_IDZIENNIK, FEATURE_ANNOUNCEMENTS, listOf( + ENDPOINT_IDZIENNIK_WEB_ANNOUNCEMENTS to LOGIN_METHOD_IDZIENNIK_WEB + ), listOf(LOGIN_METHOD_IDZIENNIK_WEB)), + + /*Feature(LOGIN_TYPE_IDZIENNIK, FEATURE_MESSAGES_INBOX, listOf( + ENDPOINT_IDZIENNIK_WEB_MESSAGES_INBOX to LOGIN_METHOD_IDZIENNIK_WEB + ), listOf(LOGIN_METHOD_IDZIENNIK_WEB)).withPriority(2), + Feature(LOGIN_TYPE_IDZIENNIK, FEATURE_MESSAGES_SENT, listOf( + ENDPOINT_IDZIENNIK_WEB_MESSAGES_SENT to LOGIN_METHOD_IDZIENNIK_WEB + ), listOf(LOGIN_METHOD_IDZIENNIK_WEB)).withPriority(2),*/ + + Feature(LOGIN_TYPE_IDZIENNIK, FEATURE_MESSAGES_INBOX, listOf( + ENDPOINT_IDZIENNIK_API_MESSAGES_INBOX to LOGIN_METHOD_IDZIENNIK_API + ), listOf(LOGIN_METHOD_IDZIENNIK_API)), + Feature(LOGIN_TYPE_IDZIENNIK, FEATURE_MESSAGES_SENT, listOf( + ENDPOINT_IDZIENNIK_API_MESSAGES_SENT to LOGIN_METHOD_IDZIENNIK_API + ), listOf(LOGIN_METHOD_IDZIENNIK_API)), + + Feature(LOGIN_TYPE_IDZIENNIK, FEATURE_LUCKY_NUMBER, listOf( + ENDPOINT_IDZIENNIK_API_CURRENT_REGISTER to LOGIN_METHOD_IDZIENNIK_API + ), listOf(LOGIN_METHOD_IDZIENNIK_API)) +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/IdziennikApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/IdziennikApi.kt new file mode 100644 index 00000000..a4370d36 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/IdziennikApi.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-29. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.callback.TextCallbackHandler +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.utils.Utils +import java.net.HttpURLConnection + +open class IdziennikApi(open val data: DataIdziennik) { + companion object { + const val TAG = "IdziennikApi" + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + fun apiGet(tag: String, endpointTemplate: String, method: Int = GET, parameters: Map = emptyMap(), onSuccess: (json: JsonElement) -> Unit) { + val endpoint = endpointTemplate.replace("\$STUDENT_ID", data.studentId ?: "") + Utils.d(tag, "Request: Idziennik/API - $IDZIENNIK_API_URL/$endpoint") + + val callback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + if (text == null) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + val json = try { + JsonParser().parse(text) + } catch (_: Exception) { null } + + var error: String? = null + if (json == null) { + error = text + } + else if (json is JsonObject) { + error = if (response?.code() == 200) null else + json.getString("message") ?: json.toString() + } + error?.let { code -> + when (code) { + "Uczeń nie posiada aktywnej pozycji w dzienniku" -> ERROR_IDZIENNIK_API_NO_REGISTER + "Authorization has been denied for this request." -> ERROR_IDZIENNIK_API_ACCESS_DENIED + else -> ERROR_IDZIENNIK_API_OTHER + }.let { errorCode -> + data.error(ApiError(tag, errorCode) + .withApiResponse(text) + .withResponse(response)) + return + } + } + + try { + onSuccess(json!!) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_IDZIENNIK_API_REQUEST) + .withResponse(response) + .withThrowable(e) + .withApiResponse(text)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + Request.builder() + .url("$IDZIENNIK_API_URL/$endpoint") + .userAgent(IDZIENNIK_API_USER_AGENT) + .addHeader("Authorization", "Bearer ${data.apiBearer}") + .apply { + when (method) { + GET -> get() + POST -> { + postJson() + val json = JsonObject() + parameters.map { (name, value) -> + when (value) { + is JsonObject -> json.add(name, value) + is JsonArray -> json.add(name, value) + is String -> json.addProperty(name, value) + is Int -> json.addProperty(name, value) + is Long -> json.addProperty(name, value) + is Float -> json.addProperty(name, value) + is Char -> json.addProperty(name, value) + } + } + setJsonBody(json) + } + } + } + .allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST) + .allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED) + .allowErrorCode(HttpURLConnection.HTTP_INTERNAL_ERROR) + .callback(callback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/IdziennikData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/IdziennikData.kt new file mode 100644 index 00000000..7b285db9 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/IdziennikData.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-25. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.api.IdziennikApiCurrentRegister +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.api.IdziennikApiMessagesInbox +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.api.IdziennikApiMessagesSent +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web.* +import pl.szczodrzynski.edziennik.utils.Utils + +class IdziennikData(val data: DataIdziennik, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "IdziennikData" + } + + init { + nextEndpoint(onSuccess) + } + + private fun nextEndpoint(onSuccess: () -> Unit) { + if (data.targetEndpointIds.isEmpty()) { + onSuccess() + return + } + if (data.cancelled) { + onSuccess() + return + } + useEndpoint(data.targetEndpointIds.removeAt(0)) { + data.progress(data.progressStep) + nextEndpoint(onSuccess) + } + } + + private fun useEndpoint(endpointId: Int, onSuccess: () -> Unit) { + Utils.d(TAG, "Using endpoint $endpointId") + when (endpointId) { + ENDPOINT_IDZIENNIK_WEB_TIMETABLE -> { + data.startProgress(R.string.edziennik_progress_endpoint_timetable) + IdziennikWebTimetable(data, onSuccess) + } + ENDPOINT_IDZIENNIK_WEB_GRADES -> { + data.startProgress(R.string.edziennik_progress_endpoint_grades) + IdziennikWebGrades(data, onSuccess) + } + ENDPOINT_IDZIENNIK_WEB_PROPOSED_GRADES -> { + data.startProgress(R.string.edziennik_progress_endpoint_proposed_grades) + IdziennikWebProposedGrades(data, onSuccess) + } + ENDPOINT_IDZIENNIK_WEB_EXAMS -> { + data.startProgress(R.string.edziennik_progress_endpoint_exams) + IdziennikWebExams(data, onSuccess) + } + ENDPOINT_IDZIENNIK_WEB_HOMEWORK -> { + data.startProgress(R.string.edziennik_progress_endpoint_homework) + IdziennikWebHomework(data, onSuccess) + } + ENDPOINT_IDZIENNIK_WEB_NOTICES -> { + data.startProgress(R.string.edziennik_progress_endpoint_notices) + IdziennikWebNotices(data, onSuccess) + } + ENDPOINT_IDZIENNIK_WEB_ANNOUNCEMENTS -> { + data.startProgress(R.string.edziennik_progress_endpoint_announcements) + IdziennikWebAnnouncements(data, onSuccess) + } + ENDPOINT_IDZIENNIK_WEB_ATTENDANCE -> { + data.startProgress(R.string.edziennik_progress_endpoint_attendance) + IdziennikWebAttendance(data, onSuccess) + } + ENDPOINT_IDZIENNIK_API_CURRENT_REGISTER -> { + data.startProgress(R.string.edziennik_progress_endpoint_lucky_number) + IdziennikApiCurrentRegister(data, onSuccess) + } + ENDPOINT_IDZIENNIK_API_MESSAGES_INBOX -> { + data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox) + IdziennikApiMessagesInbox(data, onSuccess) + } + ENDPOINT_IDZIENNIK_API_MESSAGES_SENT -> { + data.startProgress(R.string.edziennik_progress_endpoint_messages_outbox) + IdziennikApiMessagesSent(data, onSuccess) + } + else -> onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/IdziennikWeb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/IdziennikWeb.kt new file mode 100644 index 00000000..b454a2ff --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/IdziennikWeb.kt @@ -0,0 +1,211 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-25. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data + +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.callback.FileCallbackHandler +import im.wangchao.mhttp.callback.JsonCallbackHandler +import im.wangchao.mhttp.callback.TextCallbackHandler +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.io.File +import java.net.HttpURLConnection.HTTP_INTERNAL_ERROR +import java.net.HttpURLConnection.HTTP_UNAUTHORIZED + +open class IdziennikWeb(open val data: DataIdziennik) { + companion object { + const val TAG = "IdziennikWeb" + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + fun webApiGet(tag: String, endpoint: String, parameters: Map = emptyMap(), onSuccess: (json: JsonObject) -> Unit) { + d(tag, "Request: Idziennik/Web/API - $IDZIENNIK_WEB_URL/$endpoint") + + val callback = object : JsonCallbackHandler() { + override fun onSuccess(json: JsonObject?, response: Response?) { + if (json == null && response?.parserErrorBody == null) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + when { + response?.code() == HTTP_UNAUTHORIZED -> ERROR_IDZIENNIK_WEB_ACCESS_DENIED + response?.code() == HTTP_INTERNAL_ERROR -> ERROR_IDZIENNIK_WEB_SERVER_ERROR + response?.parserErrorBody != null -> when { + response.parserErrorBody.contains("Identyfikator zgłoszenia") -> ERROR_IDZIENNIK_WEB_SERVER_ERROR + response.parserErrorBody.contains("Hasło dostępu do systemu wygasło") -> ERROR_IDZIENNIK_WEB_PASSWORD_CHANGE_NEEDED + response.parserErrorBody.contains("Trwają prace konserwacyjne") -> ERROR_IDZIENNIK_WEB_MAINTENANCE + else -> ERROR_IDZIENNIK_WEB_OTHER + } + else -> null + }?.let { errorCode -> + data.error(ApiError(TAG, errorCode) + .withApiResponse(json?.toString() ?: response?.parserErrorBody) + .withResponse(response)) + return + } + + if (json == null) { + data.error(ApiError(tag, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + try { + onSuccess(json) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_IDZIENNIK_WEB_API_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("$IDZIENNIK_WEB_URL/$endpoint") + .userAgent(IDZIENNIK_USER_AGENT) + .postJson() + .apply { + val json = JsonObject() + parameters.map { (name, value) -> + when (value) { + is JsonObject -> json.add(name, value) + is JsonArray -> json.add(name, value) + is String -> json.addProperty(name, value) + is Int -> json.addProperty(name, value) + is Long -> json.addProperty(name, value) + is Float -> json.addProperty(name, value) + is Char -> json.addProperty(name, value) + is Boolean -> json.addProperty(name, value) + } + } + setJsonBody(json) + } + .allowErrorCode(HTTP_UNAUTHORIZED) + .allowErrorCode(HTTP_INTERNAL_ERROR) + .callback(callback) + .build() + .enqueue() + } + + fun webGet(tag: String, endpoint: String, onSuccess: (text: String) -> Unit) { + d(tag, "Request: Idziennik/Web - $IDZIENNIK_WEB_URL/$endpoint") + + val callback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + if (text == null) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + if (!text.contains("czyWyswietlicDostepMobilny")) { + when { + text.contains("Identyfikator zgłoszenia") -> ERROR_IDZIENNIK_WEB_SERVER_ERROR + text.contains("Hasło dostępu do systemu wygasło") -> ERROR_IDZIENNIK_WEB_PASSWORD_CHANGE_NEEDED + text.contains("Trwają prace konserwacyjne") -> ERROR_IDZIENNIK_WEB_MAINTENANCE + else -> ERROR_IDZIENNIK_WEB_OTHER + }.let { errorCode -> + data.error(ApiError(TAG, errorCode) + .withApiResponse(text) + .withResponse(response)) + return + } + } + + try { + onSuccess(text) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_IDZIENNIK_WEB_REQUEST) + .withResponse(response) + .withThrowable(e) + .withApiResponse(text)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + Request.builder() + .url("$IDZIENNIK_WEB_URL/$endpoint") + .userAgent(IDZIENNIK_USER_AGENT) + .get() + .callback(callback) + .build() + .enqueue() + } + + fun webGetFile(tag: String, endpoint: String, targetFile: File, parameters: Map, + onSuccess: (file: File) -> Unit, onProgress: (written: Long, total: Long) -> Unit) { + + d(tag, "Request: Idziennik/Web - $IDZIENNIK_WEB_URL/$endpoint") + + val callback = object : FileCallbackHandler(targetFile) { + override fun onSuccess(file: File?, response: Response?) { + if (file == null) { + data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD) + .withResponse(response)) + return + } + + try { + onSuccess(file) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_EDUDZIENNIK_FILE_REQUEST) + .withResponse(response) + .withThrowable(e)) + } + } + + override fun onProgress(bytesWritten: Long, bytesTotal: Long) { + try { + onProgress(bytesWritten, bytesTotal) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_EDUDZIENNIK_FILE_REQUEST) + .withThrowable(e)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + Request.builder() + .url("$IDZIENNIK_WEB_URL/$endpoint") + .userAgent(IDZIENNIK_USER_AGENT) + .apply { + parameters.forEach { (k, v) -> addParameter(k, v) } + } + .post() + .callback(callback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiCurrentRegister.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiCurrentRegister.kt new file mode 100644 index 00000000..1970c225 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiCurrentRegister.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-29. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.api + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.DAY +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_API_CURRENT_REGISTER +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.ENDPOINT_IDZIENNIK_API_CURRENT_REGISTER +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikApi +import pl.szczodrzynski.edziennik.data.db.modules.luckynumber.LuckyNumber +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.getInt +import pl.szczodrzynski.edziennik.getJsonObject +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +class IdziennikApiCurrentRegister(override val data: DataIdziennik, + val onSuccess: () -> Unit) : IdziennikApi(data) { + companion object { + private const val TAG = "IdziennikApiCurrentRegister" + } + + init { + data.profile?.luckyNumber = -1 + data.profile?.luckyNumberDate = null + + apiGet(TAG, IDZIENNIK_API_CURRENT_REGISTER) { json -> + if (json !is JsonObject) { + onSuccess() + return@apiGet + } + + var nextSync = System.currentTimeMillis() + 14*DAY*1000 + + val settings = json.getJsonObject("ustawienia")?.apply { + profile?.dateSemester1Start = getString("poczatekSemestru1")?.let { Date.fromY_m_d(it) } + profile?.dateSemester2Start = getString("koniecSemestru1")?.let { Date.fromY_m_d(it).stepForward(0, 0, 1) } + profile?.dateYearEnd = getString("koniecSemestru2")?.let { Date.fromY_m_d(it) } + } + + json.getInt("szczesliwyNumerek")?.let { luckyNumber -> + val luckyNumberDate = Date.getToday() + settings.getString("godzinaPublikacjiSzczesliwegoLosu") + ?.let { Time.fromH_m(it) } + ?.let { publishTime -> + val now = Time.getNow() + if (publishTime.value < 150000 && now.value < publishTime.value) { + nextSync = luckyNumberDate.combineWith(publishTime) + luckyNumberDate.stepForward(0, 0, -1) // the lucky number is still for yesterday + } + else if (publishTime.value >= 150000 && now.value > publishTime.value) { + luckyNumberDate.stepForward(0, 0, 1) // the lucky number is already for tomorrow + nextSync = luckyNumberDate.combineWith(publishTime) + } + else if (publishTime.value < 150000) { + nextSync = luckyNumberDate + .clone() + .stepForward(0, 0, 1) + .combineWith(publishTime) + } + else { + nextSync = luckyNumberDate.combineWith(publishTime) + } + } + + + val luckyNumberObject = LuckyNumber( + data.profileId, + Date.getToday(), + luckyNumber + ) + + data.luckyNumberList.add(luckyNumberObject) + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_LUCKY_NUMBER, + luckyNumberObject.date.value.toLong(), + data.profile?.empty ?: false, + data.profile?.empty ?: false, + System.currentTimeMillis() + )) + } + + + data.setSyncNext(ENDPOINT_IDZIENNIK_API_CURRENT_REGISTER, syncAt = nextSync) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiMessagesInbox.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiMessagesInbox.kt new file mode 100644 index 00000000..5e118cd5 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiMessagesInbox.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-30. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.api + +import com.google.gson.JsonArray +import pl.szczodrzynski.edziennik.asJsonObjectList +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_API_MESSAGES_INBOX +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.ENDPOINT_IDZIENNIK_API_MESSAGES_INBOX +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikApi +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_DELETED +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.getBoolean +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.utils.Utils.crc32 +import pl.szczodrzynski.edziennik.utils.models.Date + +class IdziennikApiMessagesInbox(override val data: DataIdziennik, + val onSuccess: () -> Unit) : IdziennikApi(data) { + companion object { + private const val TAG = "IdziennikApiMessagesInbox" + } + + init { + apiGet(TAG, IDZIENNIK_API_MESSAGES_INBOX) { json -> + if (json !is JsonArray) { + onSuccess() + return@apiGet + } + + json.asJsonObjectList()?.forEach { jMessage -> + val subject = jMessage.getString("tytul") + if (subject?.contains("(") == true && subject.startsWith("iDziennik - ")) + return@forEach + if (subject?.startsWith("Uwaga dla ucznia (klasa:") == true) + return@forEach + + val messageIdStr = jMessage.getString("id") + val messageId = crc32((messageIdStr + "0").toByteArray()) + + var body = "[META:$messageIdStr;-1]" + body += jMessage.getString("tresc")?.replace("\n".toRegex(), "
    ") + + val readDate = if (jMessage.getBoolean("odczytana") == true) Date.fromIso(jMessage.getString("wersjaRekordu")) else 0 + val sentDate = Date.fromIso(jMessage.getString("dataWyslania")) + + val sender = jMessage.getAsJsonObject("nadawca") + var firstName = sender.getString("imie") + var lastName = sender.getString("nazwisko") + if (firstName.isNullOrEmpty() || lastName.isNullOrEmpty()) { + firstName = "usunięty" + lastName = "użytkownik" + } + val rTeacher = data.getTeacher( + firstName, + lastName + ) + rTeacher.loginId = /*sender.getString("id") + ":" + */sender.getString("usr") + rTeacher.setTeacherType(Teacher.TYPE_OTHER) + + val message = Message( + profileId, + messageId, + subject, + body, + if (jMessage.getBoolean("rekordUsuniety") == true) TYPE_DELETED else TYPE_RECEIVED, + rTeacher.id, + -1 + ) + + val messageRecipient = MessageRecipient( + profileId, + -1 /* me */, + -1, + readDate, + /*messageId*/ messageId + ) + + data.messageIgnoreList.add(message) + data.messageRecipientList.add(messageRecipient) + data.setSeenMetadataList.add(Metadata( + profileId, + Metadata.TYPE_MESSAGE, + message.id, + readDate > 0, + readDate > 0 || profile?.empty ?: false, + sentDate + )) + } + + data.setSyncNext(ENDPOINT_IDZIENNIK_API_MESSAGES_INBOX, SYNC_ALWAYS) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiMessagesSent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiMessagesSent.kt new file mode 100644 index 00000000..e3efd69e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/api/IdziennikApiMessagesSent.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-30. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.api + +import com.google.gson.JsonArray +import pl.szczodrzynski.edziennik.DAY +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES +import pl.szczodrzynski.edziennik.asJsonObjectList +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_API_MESSAGES_SENT +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.ENDPOINT_IDZIENNIK_API_MESSAGES_SENT +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikApi +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.Utils.crc32 +import pl.szczodrzynski.edziennik.utils.models.Date + +class IdziennikApiMessagesSent(override val data: DataIdziennik, + val onSuccess: () -> Unit) : IdziennikApi(data) { + companion object { + private const val TAG = "IdziennikApiMessagesSent" + } + + init { + apiGet(TAG, IDZIENNIK_API_MESSAGES_SENT) { json -> + if (json !is JsonArray) { + onSuccess() + return@apiGet + } + + json.asJsonObjectList()?.forEach { jMessage -> + val messageIdStr = jMessage.get("id").asString + val messageId = crc32((messageIdStr + "1").toByteArray()) + + val subject = jMessage.get("tytul").asString + + var body = "[META:$messageIdStr;-1]" + body += jMessage.get("tresc").asString.replace("\n".toRegex(), "
    ") + + val sentDate = Date.fromIso(jMessage.get("dataWyslania").asString) + + val message = Message( + profileId, + messageId, + subject, + body, + TYPE_SENT, + -1, + -1 + ) + + for (recipientEl in jMessage.getAsJsonArray("odbiorcy")) { + val recipient = recipientEl.asJsonObject + var firstName = recipient.get("imie").asString + var lastName = recipient.get("nazwisko").asString + if (firstName.isEmpty() || lastName.isEmpty()) { + firstName = "usunięty" + lastName = "użytkownik" + } + val rTeacher = data.getTeacher(firstName, lastName) + rTeacher.loginId = /*recipient.get("id").asString + ":" + */recipient.get("usr").asString + + val messageRecipient = MessageRecipient( + profileId, + rTeacher.id, + -1, + -1, + /*messageId*/ messageId + ) + data.messageRecipientIgnoreList.add(messageRecipient) + } + + data.messageIgnoreList.add(message) + data.metadataList.add(Metadata(profileId, Metadata.TYPE_MESSAGE, message.id, true, true, sentDate)) + } + + data.setSyncNext(ENDPOINT_IDZIENNIK_API_MESSAGES_SENT, DAY, DRAWER_ITEM_MESSAGES) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebAnnouncements.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebAnnouncements.kt new file mode 100644 index 00000000..f8c6573b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebAnnouncements.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-28. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web + +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.data.api.ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_ANNOUNCEMENTS +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.ENDPOINT_IDZIENNIK_WEB_ANNOUNCEMENTS +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.announcements.Announcement +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.getJsonObject +import pl.szczodrzynski.edziennik.utils.models.Date + +class IdziennikWebAnnouncements(override val data: DataIdziennik, + val onSuccess: () -> Unit) : IdziennikWeb(data) { + companion object { + private const val TAG = "IdziennikWebAnnouncements" + } + + init { + val param = JsonObject() + param.add("parametryFiltrow", JsonArray()) + + webApiGet(TAG, IDZIENNIK_WEB_ANNOUNCEMENTS, mapOf( + "uczenId" to (data.studentId ?: ""), + "param" to param + )) { result -> + val json = result.getJsonObject("d") ?: run { + data.error(ApiError(TAG, ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA) + .withApiResponse(result)) + return@webApiGet + } + + for (jAnnouncementEl in json.getAsJsonArray("ListK")) { + val jAnnouncement = jAnnouncementEl.asJsonObject + // jAnnouncement + val announcementId = jAnnouncement.get("Id").asLong + + val rTeacher = data.getTeacherByFirstLast(jAnnouncement.get("Autor").asString) + val addedDate = java.lang.Long.parseLong(jAnnouncement.get("DataDodania").asString.replace("[^\\d]".toRegex(), "")) + val startDate = Date.fromMillis(java.lang.Long.parseLong(jAnnouncement.get("DataWydarzenia").asString.replace("[^\\d]".toRegex(), ""))) + + val announcementObject = Announcement( + profileId, + announcementId, + jAnnouncement.get("Temat").asString, + jAnnouncement.get("Tresc").asString, + startDate, + null, + rTeacher.id, + null + ) + data.announcementList.add(announcementObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_ANNOUNCEMENT, + announcementObject.id, + profile?.empty ?: false, + profile?.empty ?: false, + addedDate + )) + } + + data.setSyncNext(ENDPOINT_IDZIENNIK_WEB_ANNOUNCEMENTS, SYNC_ALWAYS) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebAttendance.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebAttendance.kt new file mode 100644 index 00000000..78f8e8b4 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebAttendance.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-28. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web + +import pl.szczodrzynski.edziennik.data.api.ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_ATTENDANCE +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.ENDPOINT_IDZIENNIK_WEB_ATTENDANCE +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.crc16 +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance +import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.* +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.getJsonObject +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +class IdziennikWebAttendance(override val data: DataIdziennik, + val onSuccess: () -> Unit) : IdziennikWeb(data) { + companion object { + private const val TAG = "IdziennikWebAttendance" + } + + private var attendanceYear = Date.getToday().year + private var attendanceMonth = Date.getToday().month + private var attendancePrevMonthChecked = false + + init { + getAttendance() + } + + private fun getAttendance() { + webApiGet(TAG, IDZIENNIK_WEB_ATTENDANCE, mapOf( + "idPozDziennika" to data.registerId, + "mc" to attendanceMonth, + "rok" to attendanceYear, + "dataTygodnia" to "" + )) { result -> + val json = result.getJsonObject("d") ?: run { + data.error(ApiError(TAG, ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA) + .withApiResponse(result)) + return@webApiGet + } + + for (jAttendanceEl in json.getAsJsonArray("Obecnosci")) { + val jAttendance = jAttendanceEl.asJsonObject + // jAttendance + val attendanceTypeIdziennik = jAttendance.get("TypObecnosci").asInt + if (attendanceTypeIdziennik == 5 || attendanceTypeIdziennik == 7) + continue + val attendanceDate = Date.fromY_m_d(jAttendance.get("Data").asString) + val attendanceTime = Time.fromH_m(jAttendance.get("OdDoGodziny").asString) + if (attendanceDate.combineWith(attendanceTime) > System.currentTimeMillis()) + continue + + val attendanceId = jAttendance.get("IdLesson").asString.crc16().toLong() + val rSubject = data.getSubject(jAttendance.get("Przedmiot").asString, jAttendance.get("IdPrzedmiot").asLong, "") + val rTeacher = data.getTeacherByFDotSpaceLast(jAttendance.get("PrzedmiotNauczyciel").asString) + + var attendanceName = "obecność" + var attendanceType = Attendance.TYPE_CUSTOM + + when (attendanceTypeIdziennik) { + 1 /* nieobecność usprawiedliwiona */ -> { + attendanceName = "nieobecność usprawiedliwiona" + attendanceType = TYPE_ABSENT_EXCUSED + } + 2 /* spóźnienie */ -> { + attendanceName = "spóźnienie" + attendanceType = TYPE_BELATED + } + 3 /* nieobecność nieusprawiedliwiona */ -> { + attendanceName = "nieobecność nieusprawiedliwiona" + attendanceType = TYPE_ABSENT + } + 4 /* zwolnienie */, 9 /* zwolniony / obecny */ -> { + attendanceType = TYPE_RELEASED + if (attendanceTypeIdziennik == 4) + attendanceName = "zwolnienie" + if (attendanceTypeIdziennik == 9) + attendanceName = "zwolnienie / obecność" + } + 0 /* obecny */, 8 /* Wycieczka */ -> { + attendanceType = TYPE_PRESENT + if (attendanceTypeIdziennik == 8) + attendanceName = "wycieczka" + } + } + + val semester = profile?.dateToSemester(attendanceDate) ?: 1 + + val attendanceObject = Attendance( + profileId, + attendanceId, + rTeacher.id, + rSubject.id, + semester, + attendanceName, + attendanceDate, + attendanceTime, + attendanceType + ) + + data.attendanceList.add(attendanceObject) + if (attendanceObject.type != TYPE_PRESENT) { + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_ATTENDANCE, + attendanceObject.id, + profile?.empty ?: false, + profile?.empty ?: false, + System.currentTimeMillis() + )) + } + } + + val attendanceDateValue = attendanceYear * 10000 + attendanceMonth * 100 + if (profile?.empty == true && attendanceDateValue > profile?.getSemesterStart(1)?.value ?: 99999999) { + attendancePrevMonthChecked = true // do not need to check prev month later + attendanceMonth-- + if (attendanceMonth < 1) { + attendanceMonth = 12 + attendanceYear-- + } + getAttendance() + } else if (!attendancePrevMonthChecked /* get also the previous month */) { + attendanceMonth-- + if (attendanceMonth < 1) { + attendanceMonth = 12 + attendanceYear-- + } + attendancePrevMonthChecked = true + getAttendance() + } else { + data.setSyncNext(ENDPOINT_IDZIENNIK_WEB_ATTENDANCE, SYNC_ALWAYS) + onSuccess() + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebExams.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebExams.kt new file mode 100644 index 00000000..bf32188c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebExams.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-28. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_EXAMS +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.ENDPOINT_IDZIENNIK_WEB_EXAMS +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date + +class IdziennikWebExams(override val data: DataIdziennik, + val onSuccess: () -> Unit) : IdziennikWeb(data) { + companion object { + private const val TAG = "IdziennikWebExams" + } + + private var examsYear = Date.getToday().year + private var examsMonth = Date.getToday().month + private var examsMonthsChecked = 0 + private var examsNextMonthChecked = false // TO DO temporary // no more // idk + + init { + getExams() + } + + private fun getExams() { + val param = JsonObject().apply { + addProperty("strona", 1) + addProperty("iloscNaStrone", "99") + addProperty("iloscRekordow", -1) + addProperty("kolumnaSort", "ss.Nazwa,sp.Data_sprawdzianu") + addProperty("kierunekSort", 0) + addProperty("maxIloscZaznaczonych", 0) + addProperty("panelFiltrow", 0) + } + + webApiGet(TAG, IDZIENNIK_WEB_EXAMS, mapOf( + "idP" to data.registerId, + "rok" to examsYear, + "miesiac" to examsMonth, + "param" to param + )) { result -> + val json = result.getJsonObject("d") ?: run { + data.error(ApiError(TAG, ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA) + .withApiResponse(result)) + return@webApiGet + } + + json.getJsonArray("ListK")?.asJsonObjectList()?.forEach { exam -> + val id = exam.getLong("_recordId") ?: return@forEach + val examDate = Date.fromY_m_d(exam.getString("data") ?: return@forEach) + val subjectName = exam.getString("przedmiot") ?: return@forEach + val subjectId = data.getSubject(subjectName, null, subjectName).id + val teacherName = exam.getString("wpisal") ?: return@forEach + val teacherId = data.getTeacherByLastFirst(teacherName).id + val topic = exam.getString("zakres") ?: "" + + val lessonList = data.db.timetableDao().getForDateNow(profileId, examDate) + val startTime = lessonList.firstOrNull { it.subjectId == subjectId }?.startTime + + val eventType = when (exam.getString("rodzaj")) { + "sprawdzian/praca klasowa" -> Event.TYPE_EXAM + else -> Event.TYPE_SHORT_QUIZ + } + + val eventObject = Event( + profileId, + id, + examDate, + startTime, + topic, + -1, + eventType, + false, + teacherId, + subjectId, + data.teamClass?.id ?: -1 + ) + + data.eventList.add(eventObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_EVENT, + eventObject.id, + profile?.empty ?: false, + profile?.empty ?: false, + System.currentTimeMillis() + )) + } + + if (profile?.empty == true && examsMonthsChecked < 3 /* how many months backwards to check? */) { + examsMonthsChecked++ + examsMonth-- + if (examsMonth < 1) { + examsMonth = 12 + examsYear-- + } + getExams() + } else if (!examsNextMonthChecked /* get also one month forward */) { + val showDate = Date.getToday().stepForward(0, 1, 0) + examsYear = showDate.year + examsMonth = showDate.month + examsNextMonthChecked = true + getExams() + } else { + data.toRemove.add(DataRemoveModel.Events.futureExceptType(Event.TYPE_HOMEWORK)) + + data.setSyncNext(ENDPOINT_IDZIENNIK_WEB_EXAMS, SYNC_ALWAYS) + onSuccess() + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetAttachment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetAttachment.kt new file mode 100644 index 00000000..16e5ce7f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetAttachment.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-28 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_GET_ATTACHMENT +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.utils.Utils +import java.io.File + +class IdziennikWebGetAttachment( + override val data: DataIdziennik, val message: Message, val attachmentId: Long, + val attachmentName: String, val onSuccess: () -> Unit +) : IdziennikWeb(data) { + companion object { + const val TAG = "IdziennikWebGetAttachment" + } + + init { + val messageId = "\\[META:([A-z0-9]+);([0-9-]+)]".toRegex().find(message.body ?: "")?.get(2) ?: -1 + val targetFile = File(Utils.getStorageDir(), attachmentName) + + webGetFile(TAG, IDZIENNIK_WEB_GET_ATTACHMENT, targetFile, mapOf( + "id" to messageId, + "fileName" to attachmentName + ), { file -> + val event = AttachmentGetEvent( + profileId, + message.id, + attachmentId, + AttachmentGetEvent.TYPE_FINISHED, + file.absolutePath + ) + + val attachmentDataFile = File(Utils.getStorageDir(), ".${profileId}_${event.messageId}_${event.attachmentId}") + Utils.writeStringToFile(attachmentDataFile, event.fileName) + + EventBus.getDefault().post(event) + + onSuccess() + + }) { written, _ -> + val event = AttachmentGetEvent( + profileId, + message.id, + attachmentId, + AttachmentGetEvent.TYPE_PROGRESS, + bytesWritten = written + ) + + EventBus.getDefault().post(event) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetMessage.kt new file mode 100644 index 00000000..b0d372de --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetMessage.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-28 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_GET_MESSAGE +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipientFull +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date + +class IdziennikWebGetMessage( + override val data: DataIdziennik, + private val message: MessageFull, + val onSuccess: () -> Unit +) : IdziennikWeb(data) { + companion object { + const val TAG = "IdziennikWebGetMessage" + } + + init { data.profile?.also { profile -> + val metaPattern = "\\[META:([A-z0-9]+);([0-9-]+)]".toRegex() + val meta = metaPattern.find(message.body!!) + val messageIdString = meta?.get(1) ?: "" + + webApiGet(TAG, IDZIENNIK_WEB_GET_MESSAGE, parameters = mapOf( + "idWiadomosci" to messageIdString, + "typWiadomosci" to if (message.type == TYPE_SENT) 1 else 0 + )) { json -> + json.getJsonObject("d")?.getJsonObject("Wiadomosc")?.also { + val id = it.getLong("_recordId") + message.body = message.body?.replace(metaPattern, "[META:$messageIdString;$id]") + + message.clearAttachments() + it.getJsonArray("ListaZal")?.asJsonObjectList()?.forEach { attachment -> + message.addAttachment( + attachment.getLong("Id") ?: return@forEach, + attachment.getString("Nazwa") ?: return@forEach, + -1 + ) + } + + message.recipients?.clear() + when (message.type) { + TYPE_RECEIVED -> { + val recipientObject = MessageRecipientFull(profileId, -1, message.id) + + val readDateString = it.getString("DataOdczytania") + recipientObject.readDate = if (readDateString.isNullOrBlank()) System.currentTimeMillis() + else Date.fromIso(readDateString) + + recipientObject.fullName = profile.accountNameLong ?: profile.studentNameLong + + data.messageRecipientList.add(recipientObject) + message.addRecipient(recipientObject) + } + + TYPE_SENT -> { + it.getJsonArray("ListaOdbiorcow")?.asJsonObjectList()?.forEach { recipient -> + val recipientName = recipient.getString("NazwaOdbiorcy") ?: return@forEach + val teacher = data.getTeacherByLastFirst(recipientName) + + val recipientObject = MessageRecipientFull(profileId, teacher.id, message.id) + + recipientObject.readDate = recipient.getLong("Status") ?: return@forEach + recipientObject.fullName = teacher.fullName + + data.messageRecipientList.add(recipientObject) + message.addRecipient(recipientObject) + } + } + } + + if (!message.seen) { + message.seen = true + + data.setSeenMetadataList.add(Metadata( + profileId, + Metadata.TYPE_MESSAGE, + message.id, + message.seen, + message.notified, + message.addedDate + )) + } + + EventBus.getDefault().postSticky(MessageGetEvent(message)) + + data.messageList.add(message) + onSuccess() + } + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetRecipientList.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetRecipientList.kt new file mode 100644 index 00000000..f82b05e2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGetRecipientList.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-30. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web + +import androidx.room.OnConflictStrategy +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_GET_RECIPIENT_LIST +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher + +class IdziennikWebGetRecipientList( + override val data: DataIdziennik, val onSuccess: () -> Unit) : IdziennikWeb(data) { + companion object { + private const val TAG = "IdziennikWebGetRecipientList" + } + + init { + webApiGet(TAG, IDZIENNIK_WEB_GET_RECIPIENT_LIST, mapOf( + "idP" to data.registerId + )) { result -> + val json = result.getJsonObject("d") ?: run { + data.error(ApiError(TAG, ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA) + .withApiResponse(result)) + return@webApiGet + } + + json.getJsonArray("ListK_Pracownicy")?.asJsonObjectList()?.forEach { recipient -> + val name = recipient.getString("ImieNazwisko") ?: ": " + val (fullName, subject) = name.split(": ").let { + Pair(it.getOrNull(0), it.getOrNull(1)) + } + val guid = recipient.getString("Id") ?: "" + // get teacher by ID or create it + val teacher = data.getTeacherByFirstLast(fullName ?: " ") + teacher.loginId = guid + teacher.setTeacherType(Teacher.TYPE_TEACHER) + // unset OTHER that is automatically set in IdziennikApiMessages* + teacher.unsetTeacherType(Teacher.TYPE_OTHER) + teacher.typeDescription = subject + } + + json.getJsonArray("ListK_Opiekunowie")?.asJsonObjectList()?.forEach { recipient -> + val name = recipient.getString("ImieNazwisko") ?: ": " + val (fullName, parentOf) = Regexes.IDZIENNIK_MESSAGES_RECIPIENT_PARENT.find(name)?.let { + Pair(it.groupValues.getOrNull(1), it.groupValues.getOrNull(2)) + } ?: Pair(null, null) + val guid = recipient.getString("Id") ?: "" + // get teacher by ID or create it + val teacher = data.getTeacherByFirstLast(fullName ?: " ") + teacher.loginId = guid + teacher.setTeacherType(Teacher.TYPE_PARENT) + // unset OTHER that is automatically set in IdziennikApiMessages* + teacher.unsetTeacherType(Teacher.TYPE_OTHER) + teacher.typeDescription = parentOf + } + + val event = RecipientListGetEvent( + data.profileId, + data.teacherList.filter { it.loginId != null } + ) + + profile?.lastReceiversSync = System.currentTimeMillis() + + data.teacherOnConflictStrategy = OnConflictStrategy.REPLACE + EventBus.getDefault().postSticky(event) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGrades.kt new file mode 100644 index 00000000..fa9f98c3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebGrades.kt @@ -0,0 +1,163 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-28. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web + +import android.graphics.Color +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.ENDPOINT_IDZIENNIK_WEB_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date + +class IdziennikWebGrades(override val data: DataIdziennik, + val onSuccess: () -> Unit) : IdziennikWeb(data) { + companion object { + private const val TAG = "IdziennikWebGrades" + } + + init { data.profile?.also { profile -> + webApiGet(TAG, IDZIENNIK_WEB_GRADES, mapOf( + "idPozDziennika" to data.registerId + )) { result -> + val json = result.getJsonObject("d") ?: run { + data.error(ApiError(TAG, ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA) + .withApiResponse(result)) + return@webApiGet + } + + json.getJsonArray("Przedmioty")?.asJsonObjectList()?.onEach { subjectJson -> + val subject = data.getSubject( + subjectJson.getString("Przedmiot") ?: return@onEach, + subjectJson.getLong("IdPrzedmiotu") ?: return@onEach, + subjectJson.getString("Przedmiot") ?: return@onEach + ) + subjectJson.getJsonArray("Oceny")?.asJsonObjectList()?.forEach { grade -> + val id = grade.getLong("idK") ?: return@forEach + val category = grade.getString("Kategoria") ?: "" + val name = grade.getString("Ocena") ?: "?" + val semester = grade.getInt("Semestr") ?: 1 + val teacher = data.getTeacherByLastFirst(grade.getString("Wystawil") ?: return@forEach) + + val countToAverage = grade.getBoolean("DoSredniej") ?: true + var value = grade.getFloat("WartoscDoSred") ?: 0.0f + val weight = if (countToAverage) + grade.getFloat("Waga") ?: 0.0f + else + 0.0f + + val gradeColor = grade.getString("Kolor") ?: "" + var colorInt = 0xff2196f3.toInt() + if (gradeColor.isNotEmpty()) { + colorInt = Color.parseColor("#$gradeColor") + } + + val gradeObject = Grade( + profileId, + id, + category, + colorInt, + "", + name, + value, + weight, + semester, + teacher.id, + subject.id) + + when (grade.getInt("Typ")) { + 0 -> { + val history = grade.getJsonArray("Historia")?.asJsonObjectList() + if (history?.isNotEmpty() == true) { + var sum = gradeObject.value * gradeObject.weight + var count = gradeObject.weight + for (historyItem in history) { + val countToTheAverage = historyItem.getBoolean("DoSredniej") ?: false + value = historyItem.get("WartoscDoSred").asFloat + val weight = historyItem.get("Waga").asFloat + + if (value > 0 && countToTheAverage) { + sum += value * weight + count += weight + } + + val historyObject = Grade( + profileId, + gradeObject.id * -1, + historyItem.get("Kategoria").asString, + Color.parseColor("#" + historyItem.get("Kolor").asString), + historyItem.get("Uzasadnienie").asString, + historyItem.get("Ocena").asString, + value, + if (value > 0f && countToTheAverage) weight * -1f else 0f, + historyItem.get("Semestr").asInt, + teacher.id, + subject.id) + historyObject.parentId = gradeObject.id + + val addedDate = historyItem.getString("Data_wystaw")?.let { Date.fromY_m_d(it).inMillis } ?: System.currentTimeMillis() + + data.gradeList.add(historyObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_GRADE, + historyObject.id, + true, + true, + addedDate + )) + } + // update the current grade's value with an average of all historical grades and itself + if (sum > 0 && count > 0) { + gradeObject.value = sum / count + } + gradeObject.isImprovement = true // gradeObject is the improved grade. Originals are historyObjects + } + } + 1 -> { + gradeObject.type = Grade.TYPE_SEMESTER1_FINAL + gradeObject.name = value.toInt().toString() + gradeObject.weight = 0f + } + 2 -> { + gradeObject.type = Grade.TYPE_YEAR_FINAL + gradeObject.name = value.toInt().toString() + gradeObject.weight = 0f + } + } + + val addedDate = grade.getString("Data_wystaw")?.let { Date.fromY_m_d(it).inMillis } ?: System.currentTimeMillis() + + data.gradeList.add(gradeObject) + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_GRADE, + id, + data.profile.empty, + data.profile.empty, + addedDate + )) + } + } + + data.toRemove.addAll(listOf( + Grade.TYPE_NORMAL, + Grade.TYPE_SEMESTER1_FINAL, + Grade.TYPE_YEAR_FINAL + ).map { + DataRemoveModel.Grades.semesterWithType(profile.currentSemester, it) + }) + data.setSyncNext(ENDPOINT_IDZIENNIK_WEB_GRADES, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebHomework.kt new file mode 100644 index 00000000..7cfc5b91 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebHomework.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-11-25 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_HOMEWORK +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.ENDPOINT_IDZIENNIK_WEB_HOMEWORK +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date + +class IdziennikWebHomework(override val data: DataIdziennik, + val onSuccess: () -> Unit) : IdziennikWeb(data) { + companion object { + private const val TAG = "IdziennikWebHomework" + } + + init { + val param = JsonObject().apply { + addProperty("strona", 1) + addProperty("iloscNaStrone", 997) + addProperty("iloscRekordow", -1) + addProperty("kolumnaSort", "DataZadania") + addProperty("kierunekSort", 0) + addProperty("maxIloscZaznaczonych", 0) + addProperty("panelFiltrow", 0) + } + + webApiGet(TAG, IDZIENNIK_WEB_HOMEWORK, mapOf( + "idP" to data.registerId, + "data" to Date.getToday().stringY_m_d, + "wszystkie" to true, + "param" to param + )) { result -> + val json = result.getJsonObject("d") ?: run { + data.error(ApiError(TAG, ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA) + .withApiResponse(result)) + return@webApiGet + } + + json.getJsonArray("ListK")?.asJsonObjectList()?.forEach { homework -> + val id = homework.getLong("_recordId") ?: return@forEach + val eventDate = Date.fromY_m_d(homework.getString("dataO") ?: return@forEach) + val subjectName = homework.getString("przed") ?: return@forEach + val subjectId = data.getSubject(subjectName, null, subjectName).id + val teacherName = homework.getString("usr") ?: return@forEach + val teacherId = data.getTeacherByLastFirst(teacherName).id + val lessonList = data.db.timetableDao().getForDateNow(profileId, eventDate) + val startTime = lessonList.firstOrNull { it.subjectId == subjectId }?.displayStartTime + val topic = homework.getString("tytul") ?: "" + + val seen = when (profile?.empty) { + true -> true + else -> eventDate < Date.getToday() + } + + + val eventObject = Event( + profileId, + id, + eventDate, + startTime, + topic, + -1, + Event.TYPE_HOMEWORK, + false, + teacherId, + subjectId, + data.teamClass?.id ?: -1 + ) + + data.eventList.add(eventObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_HOMEWORK, + eventObject.id, + seen, + seen, + System.currentTimeMillis() + )) + } + + data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_HOMEWORK)) + + data.setSyncNext(ENDPOINT_IDZIENNIK_WEB_HOMEWORK, SYNC_ALWAYS) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebNotices.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebNotices.kt new file mode 100644 index 00000000..cfd0d5f6 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebNotices.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-28. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web + +import pl.szczodrzynski.edziennik.data.api.ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_NOTICES +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.ENDPOINT_IDZIENNIK_WEB_NOTICES +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.crc16 +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.notices.Notice +import pl.szczodrzynski.edziennik.data.db.modules.notices.Notice.* +import pl.szczodrzynski.edziennik.getJsonObject +import pl.szczodrzynski.edziennik.utils.models.Date + +class IdziennikWebNotices(override val data: DataIdziennik, + val onSuccess: () -> Unit) : IdziennikWeb(data) { + companion object { + private const val TAG = "IdziennikWebNotices" + } + + init { + webApiGet(TAG, IDZIENNIK_WEB_NOTICES, mapOf( + "idPozDziennika" to data.registerId + )) { result -> + val json = result.getJsonObject("d") ?: run { + data.error(ApiError(TAG, ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA) + .withApiResponse(result)) + return@webApiGet + } + + for (jNoticeEl in json.getAsJsonArray("SUwaga")) { + val jNotice = jNoticeEl.asJsonObject + // jNotice + val noticeId = jNotice.get("id").asString.crc16().toLong() + + val rTeacher = data.getTeacherByLastFirst(jNotice.get("Nauczyciel").asString) + val addedDate = Date.fromY_m_d(jNotice.get("Data").asString) + + var nType = TYPE_NEUTRAL + val jType = jNotice.get("Typ").asString + if (jType == "n") { + nType = TYPE_NEGATIVE + } else if (jType == "p") { + nType = TYPE_POSITIVE + } + + val noticeObject = Notice( + profileId, + noticeId, + jNotice.get("Tresc").asString, + jNotice.get("Semestr").asInt, + nType, + rTeacher.id) + data.noticeList.add(noticeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_NOTICE, + noticeObject.id, + profile?.empty ?: false, + profile?.empty ?: false, + addedDate.inMillis + )) + } + + data.setSyncNext(ENDPOINT_IDZIENNIK_WEB_NOTICES, SYNC_ALWAYS) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebProposedGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebProposedGrades.kt new file mode 100644 index 00000000..f82827bf --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebProposedGrades.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-28. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web + +import pl.szczodrzynski.edziennik.asJsonObjectList +import pl.szczodrzynski.edziennik.data.api.ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_MISSING_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.ENDPOINT_IDZIENNIK_WEB_PROPOSED_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_SEMESTER1_PROPOSED +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_YEAR_PROPOSED +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.getJsonArray +import pl.szczodrzynski.edziennik.getJsonObject +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.utils.Utils.getWordGradeValue + +class IdziennikWebProposedGrades(override val data: DataIdziennik, + val onSuccess: () -> Unit) : IdziennikWeb(data) { + companion object { + private const val TAG = "IdziennikWebProposedGrades" + } + + init { data.profile?.also { profile -> + webApiGet(TAG, IDZIENNIK_WEB_MISSING_GRADES, mapOf( + "idPozDziennika" to data.registerId + )) { result -> + val json = result.getJsonObject("d") ?: run { + data.error(ApiError(TAG, ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA) + .withApiResponse(result)) + return@webApiGet + } + + json.getJsonArray("Przedmioty")?.asJsonObjectList()?.forEach { subject -> + val subjectName = subject.getString("Przedmiot") ?: return@forEach + val subjectObject = data.getSubject(subjectName, null, subjectName) + + val semester1Proposed = subject.getString("OcenaSem1") ?: "" + val semester1Value = getWordGradeValue(semester1Proposed) + val semester1Id = subjectObject.id * (-100) - 1 + + val semester2Proposed = subject.getString("OcenaSem2") ?: "" + val semester2Value = getWordGradeValue(semester2Proposed) + val semester2Id = subjectObject.id * (-100) - 2 + + if (semester1Proposed != "") { + val gradeObject = Grade( + profileId, + semester1Id, + "", + -1, + "", + semester1Value.toString(), + semester1Value.toFloat(), + 0f, + 1, + -1, + subjectObject.id + ).apply { + type = TYPE_SEMESTER1_PROPOSED + } + + data.gradeList.add(gradeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_GRADE, + gradeObject.id, + profile.empty, + profile.empty, + System.currentTimeMillis() + )) + } + + if (semester2Proposed != "") { + val gradeObject = Grade( + profileId, + semester2Id, + "", + -1, + "", + semester2Value.toString(), + semester2Value.toFloat(), + 0f, + 2, + -1, + subjectObject.id + ).apply { + type = TYPE_YEAR_PROPOSED + } + + data.gradeList.add(gradeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_GRADE, + gradeObject.id, + profile.empty, + profile.empty, + System.currentTimeMillis() + )) + } + } + + data.toRemove.addAll(listOf(TYPE_SEMESTER1_PROPOSED, TYPE_YEAR_PROPOSED).map { + DataRemoveModel.Grades.semesterWithType(profile.currentSemester, it) + }) + data.setSyncNext(ENDPOINT_IDZIENNIK_WEB_PROPOSED_GRADES, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebSendMessage.kt new file mode 100644 index 00000000..4d79d869 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebSendMessage.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-30. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_SEND_MESSAGE +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.api.IdziennikApiMessagesSent +import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import java.util.* + +class IdziennikWebSendMessage( + override val data: DataIdziennik, + val recipients: List, + val subject: String, + val text: String, + val onSuccess: () -> Unit +) : IdziennikWeb(data) { + companion object { + private const val TAG = "IdziennikWebSendMessage" + } + + init { + val recipientsArray = JsonArray() + for (teacher in recipients) { + teacher.loginId?.let { + recipientsArray += it + } + } + + webApiGet(TAG, IDZIENNIK_WEB_SEND_MESSAGE, mapOf( + "Wiadomosc" to JsonObject( + "Tytul" to subject, + "Tresc" to text, + "Confirmation" to false, + "GuidMessage" to UUID.randomUUID().toString().toUpperCase(Locale.ROOT), + "Odbiorcy" to recipientsArray + ) + )) { result -> + val json = result.getJsonObject("d") ?: run { + data.error(ApiError(TAG, ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA) + .withApiResponse(result)) + return@webApiGet + } + + if (json.getBoolean("CzyJestBlad") != false) { + // TODO error + return@webApiGet + } + + IdziennikApiMessagesSent(data) { + val message = data.messageIgnoreList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject } + val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id } + val event = MessageSentEvent(data.profileId, message, metadata?.addedDate) + + EventBus.getDefault().postSticky(event) + onSuccess() + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebTimetable.kt new file mode 100644 index 00000000..d85e5692 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/data/web/IdziennikWebTimetable.kt @@ -0,0 +1,192 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-11-22 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web + +import androidx.core.util.set +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_TIMETABLE +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.ENDPOINT_IDZIENNIK_WEB_TIMETABLE +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonRange +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson +import pl.szczodrzynski.edziennik.utils.Utils.d +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.edziennik.utils.models.Week + +class IdziennikWebTimetable(override val data: DataIdziennik, + val onSuccess: () -> Unit) : IdziennikWeb(data) { + companion object { + private const val TAG = "IdziennikWebTimetable" + } + + init { data.profile?.also { profile -> + val currentWeekStart = Week.getWeekStart() + + if (Date.getToday().weekDay > 4) { + currentWeekStart.stepForward(0, 0, 7) + } + + val getDate = data.arguments?.getString("weekStart") ?: currentWeekStart.stringY_m_d + + val weekStart = Date.fromY_m_d(getDate) + val weekEnd = weekStart.clone().stepForward(0, 0, 6) + + webApiGet(TAG, IDZIENNIK_WEB_TIMETABLE, mapOf( + "idPozDziennika" to data.registerId, + "pidRokSzkolny" to data.schoolYearId, + "data" to "${weekStart.stringY_m_d}T10:00:00.000Z" + )) { result -> + val json = result.getJsonObject("d") ?: run { + data.error(ApiError(TAG, ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA) + .withApiResponse(result)) + return@webApiGet + } + + json.getJsonArray("GodzinyLekcyjne")?.asJsonObjectList()?.forEach { range -> + val lessonRange = LessonRange( + profileId, + range.getInt("LiczbaP") ?: return@forEach, + range.getString("Poczatek")?.let { Time.fromH_m(it) } ?: return@forEach, + range.getString("Koniec")?.let { Time.fromH_m(it) } ?: return@forEach + ) + data.lessonRanges[lessonRange.lessonNumber] = lessonRange + } + + val dates = mutableSetOf() + val lessons = mutableListOf() + + json.getJsonArray("Przedmioty")?.asJsonObjectList()?.forEach { lesson -> + val subject = data.getSubject( + lesson.getString("Nazwa") ?: return@forEach, + lesson.getLong("Id"), + lesson.getString("Skrot") ?: "" + ) + val teacher = data.getTeacherByFDotLast(lesson.getString("Nauczyciel") + ?: return@forEach) + + val newSubjectName = lesson.getString("PrzedmiotZastepujacy") + val newSubject = when (newSubjectName.isNullOrBlank()) { + true -> null + else -> data.getSubject(newSubjectName, null, newSubjectName) + } + + val newTeacherName = lesson.getString("NauZastepujacy") + val newTeacher = when (newTeacherName.isNullOrBlank()) { + true -> null + else -> data.getTeacherByFDotLast(newTeacherName) + } + + val weekDay = lesson.getInt("DzienTygodnia")?.minus(1) ?: return@forEach + val lessonRange = data.lessonRanges[lesson.getInt("Godzina")?.plus(1) + ?: return@forEach] + val lessonDate = weekStart.clone().stepForward(0, 0, weekDay) + val classroom = lesson.getString("NazwaSali") + + val type = lesson.getInt("TypZastepstwa") ?: -1 + + val lessonObject = Lesson(profileId, -1) + + when (type) { + 1, 2, 3, 4, 5 -> { + lessonObject.apply { + this.type = Lesson.TYPE_CHANGE + + this.date = lessonDate + this.lessonNumber = lessonRange.lessonNumber + this.startTime = lessonRange.startTime + this.endTime = lessonRange.endTime + this.subjectId = newSubject?.id + this.teacherId = newTeacher?.id + this.teamId = data.teamClass?.id + this.classroom = classroom + + this.oldDate = lessonDate + this.oldLessonNumber = lessonRange.lessonNumber + this.oldStartTime = lessonRange.startTime + this.oldEndTime = lessonRange.endTime + this.oldSubjectId = subject.id + this.oldTeacherId = teacher.id + this.oldTeamId = data.teamClass?.id + this.oldClassroom = classroom + } + } + 0 -> { + lessonObject.apply { + this.type = Lesson.TYPE_CANCELLED + + this.oldDate = lessonDate + this.oldLessonNumber = lessonRange.lessonNumber + this.oldStartTime = lessonRange.startTime + this.oldEndTime = lessonRange.endTime + this.oldSubjectId = subject.id + this.oldTeacherId = teacher.id + this.oldTeamId = data.teamClass?.id + this.oldClassroom = classroom + } + } + else -> { + lessonObject.apply { + this.type = Lesson.TYPE_NORMAL + + this.date = lessonDate + this.lessonNumber = lessonRange.lessonNumber + this.startTime = lessonRange.startTime + this.endTime = lessonRange.endTime + this.subjectId = subject.id + this.teacherId = teacher.id + this.teamId = data.teamClass?.id + this.classroom = classroom + } + } + } + + lessonObject.id = lessonObject.buildId() + + dates.add(lessonDate.value) + lessons.add(lessonObject) + + val seen = profile.empty || lessonDate < Date.getToday() + + if (lessonObject.type != Lesson.TYPE_NORMAL && lessonDate >= Date.getToday()) { + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_LESSON_CHANGE, + lessonObject.id, + seen, + seen, + System.currentTimeMillis() + )) + } + } + + val date: Date = weekStart.clone() + while (date <= weekEnd) { + if (!dates.contains(date.value)) { + lessons.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 ${weekStart.stringY_m_d} and ${weekEnd.stringY_m_d} - timetable downloaded for $getDate") + + data.lessonNewList.addAll(lessons) + data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd)) + + data.setSyncNext(ENDPOINT_IDZIENNIK_WEB_TIMETABLE, SYNC_ALWAYS) + onSuccess() + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/firstlogin/IdziennikFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/firstlogin/IdziennikFirstLogin.kt new file mode 100644 index 00000000..6ef9950f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/firstlogin/IdziennikFirstLogin.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-27. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.firstlogin + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_IDZIENNIK_FIRST_NO_SCHOOL_YEAR +import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_SETTINGS +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.login.IdziennikLoginWeb +import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.fixName +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.swapFirstLastName + +class IdziennikFirstLogin(val data: DataIdziennik, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "IdziennikFirstLogin" + } + + private val web = IdziennikWeb(data) + private val profileList = mutableListOf() + + init { + IdziennikLoginWeb(data) { + web.webGet(TAG, IDZIENNIK_WEB_SETTINGS) { text -> + //val accounts = json.getJsonArray("accounts") + + val isParent = Regexes.IDZIENNIK_LOGIN_FIRST_IS_PARENT.find(text)?.get(1) != "0" + val accountNameLong = if (isParent) + Regexes.IDZIENNIK_LOGIN_FIRST_ACCOUNT_NAME.find(text)?.get(1)?.swapFirstLastName()?.fixName() + else + null + + var schoolYearName: String? = null + val schoolYear = Regexes.IDZIENNIK_LOGIN_FIRST_SCHOOL_YEAR.find(text)?.let { + schoolYearName = it[2] + it[1].toIntOrNull() + } ?: run { + data.error(ApiError(TAG, ERROR_LOGIN_IDZIENNIK_FIRST_NO_SCHOOL_YEAR) + .withApiResponse(text)) + return@webGet + } + + Regexes.IDZIENNIK_LOGIN_FIRST_STUDENT.findAll(text) + .toMutableList() + .reversed() + .forEach { match -> + val registerId = match[1].toIntOrNull() ?: return@forEach + val studentId = match[2] + val firstName = match[3] + val lastName = match[4] + val className = match[5] + " " + match[6] + + val profile = Profile() + profile.studentNameLong = "$firstName $lastName".fixName() + profile.studentNameShort = "$firstName ${lastName[0]}.".fixName() + profile.accountNameLong = accountNameLong + profile.studentClassName = className + profile.studentSchoolYear = schoolYearName + profile.name = profile.studentNameLong + profile.subname = data.webUsername + profile.empty = true + profile.putStudentData("studentId", studentId) + profile.putStudentData("registerId", registerId) + profile.putStudentData("schoolYearId", schoolYear) + profileList.add(profile) + } + + EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore)) + onSuccess() + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/login/IdziennikLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/login/IdziennikLogin.kt new file mode 100644 index 00000000..e1cef5c1 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/login/IdziennikLogin.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-25. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.login + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_IDZIENNIK_API +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_IDZIENNIK_WEB +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.utils.Utils + +class IdziennikLogin(val data: DataIdziennik, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "IdziennikLogin" + } + + private var cancelled = false + + init { + nextLoginMethod(onSuccess) + } + + private fun nextLoginMethod(onSuccess: () -> Unit) { + if (data.targetLoginMethodIds.isEmpty()) { + onSuccess() + return + } + if (cancelled) { + onSuccess() + return + } + useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId -> + data.progress(data.progressStep) + if (usedMethodId != -1) + data.loginMethods.add(usedMethodId) + nextLoginMethod(onSuccess) + } + } + + private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) { + // this should never be true + if (data.loginMethods.contains(loginMethodId)) { + onSuccess(-1) + return + } + Utils.d(TAG, "Using login method $loginMethodId") + when (loginMethodId) { + LOGIN_METHOD_IDZIENNIK_WEB -> { + data.startProgress(R.string.edziennik_progress_login_idziennik_web) + IdziennikLoginWeb(data) { onSuccess(loginMethodId) } + } + LOGIN_METHOD_IDZIENNIK_API -> { + data.startProgress(R.string.edziennik_progress_login_idziennik_api) + IdziennikLoginApi(data) { onSuccess(loginMethodId) } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/login/IdziennikLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/login/IdziennikLoginApi.kt new file mode 100644 index 00000000..2afe481d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/login/IdziennikLoginApi.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-27. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.login + +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik + +class IdziennikLoginApi(val data: DataIdziennik, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "IdziennikLoginApi" + } + + init { run { + if (data.isApiLoginValid()) { + onSuccess() + } + else { + onSuccess() + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/login/IdziennikLoginWeb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/login/IdziennikLoginWeb.kt new file mode 100644 index 00000000..9f10268e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/idziennik/login/IdziennikLoginWeb.kt @@ -0,0 +1,148 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-26. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.login + +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.callback.TextCallbackHandler +import okhttp3.Cookie +import pl.szczodrzynski.edziennik.HOUR +import pl.szczodrzynski.edziennik.MINUTE +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.getUnixDate +import pl.szczodrzynski.edziennik.utils.Utils + +class IdziennikLoginWeb(val data: DataIdziennik, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "IdziennikLoginWeb" + } + + init { run { + if (data.isWebLoginValid()) { + data.app.cookieJar.saveFromResponse(null, listOf( + Cookie.Builder() + .name("ASP.NET_SessionId_iDziennik") + .value(data.webSessionId!!) + .domain("iuczniowie.progman.pl") + .secure().httpOnly().build(), + Cookie.Builder() + .name(".ASPXAUTH") + .value(data.webAuth!!) + .domain("iuczniowie.progman.pl") + .secure().httpOnly().build() + )) + onSuccess() + } + else { + data.app.cookieJar.clearForDomain("iuczniowie.progman.pl") + if (data.webSchoolName != null && data.webUsername != null && data.webPassword != null) { + loginWithCredentials() + } + else { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + } + } + }} + + private fun loginWithCredentials() { + Utils.d(TAG, "Request: Idziennik/Login/Web - $IDZIENNIK_WEB_URL/$IDZIENNIK_WEB_LOGIN") + + val loginCallback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + if (text.isNullOrEmpty()) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + // login succeeded: there is a start page + if (text.contains("czyWyswietlicDostepMobilny")) { + val cookies = data.app.cookieJar.getForDomain("iuczniowie.progman.pl") + run { + data.webSessionId = cookies.singleOrNull { it.name() == "ASP.NET_SessionId_iDziennik" }?.value() ?: return@run ERROR_LOGIN_IDZIENNIK_WEB_NO_SESSION + data.webAuth = cookies.singleOrNull { it.name() == ".ASPXAUTH" }?.value() ?: return@run ERROR_LOGIN_IDZIENNIK_WEB_NO_AUTH + data.apiBearer = cookies.singleOrNull { it.name() == "Bearer" }?.value() ?: return@run ERROR_LOGIN_IDZIENNIK_WEB_NO_BEARER + data.loginExpiryTime = response.getUnixDate() + 30 * MINUTE /* after about 40 minutes the login didn't work already */ + data.apiExpiryTime = response.getUnixDate() + 12 * HOUR /* actually it expires after 24 hours but I'm not sure when does the token refresh. */ + return@run null + }?.let { errorCode -> + data.error(ApiError(TAG, errorCode) + .withApiResponse(text) + .withResponse(response)) + return + } + + onSuccess() + return + } + + val errorText = Regexes.IDZIENNIK_LOGIN_ERROR.find(text)?.get(1) + when { + errorText?.contains("nieprawidłową nazwę szkoły") == true -> ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME + errorText?.contains("nieprawidłowy login lub hasło") == true -> ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN + text.contains("Identyfikator zgłoszenia") -> ERROR_LOGIN_IDZIENNIK_WEB_SERVER_ERROR + text.contains("Hasło dostępu do systemu wygasło") -> ERROR_LOGIN_IDZIENNIK_WEB_PASSWORD_CHANGE_NEEDED + text.contains("Trwają prace konserwacyjne") -> ERROR_LOGIN_IDZIENNIK_WEB_MAINTENANCE + else -> ERROR_LOGIN_IDZIENNIK_WEB_OTHER + }.let { errorCode -> + data.error(ApiError(TAG, errorCode) + .withApiResponse(text) + .withResponse(response)) + return + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + val getCallback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + Request.builder() + .url("$IDZIENNIK_WEB_URL/$IDZIENNIK_WEB_LOGIN") + .userAgent(IDZIENNIK_USER_AGENT) + .addHeader("Origin", "https://iuczniowie.progman.pl") + .addHeader("Referer", "$IDZIENNIK_WEB_URL/$IDZIENNIK_WEB_LOGIN") + .apply { + Regexes.IDZIENNIK_LOGIN_HIDDEN_FIELDS.findAll(text ?: return@apply).forEach { + addParameter(it[1], it[2]) + } + } + .addParameter("ctl00\$ContentPlaceHolder\$nazwaPrzegladarki", IDZIENNIK_USER_AGENT) + .addParameter("ctl00\$ContentPlaceHolder\$NazwaSzkoly", data.webSchoolName) + .addParameter("ctl00\$ContentPlaceHolder\$UserName", data.webUsername) + .addParameter("ctl00\$ContentPlaceHolder\$Password", data.webPassword) + .addParameter("ctl00\$ContentPlaceHolder\$captcha", "") + .addParameter("ctl00\$ContentPlaceHolder\$Logowanie", "Zaloguj") + .post() + .allowErrorCode(502) + .callback(loginCallback) + .build() + .enqueue() + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + Request.builder() + .url("$IDZIENNIK_WEB_URL/$IDZIENNIK_WEB_LOGIN") + .userAgent(IDZIENNIK_USER_AGENT) + .get() + .allowErrorCode(502) + .callback(getCallback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/DataLibrus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/DataLibrus.kt new file mode 100644 index 00000000..a4ca17ba --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/DataLibrus.kt @@ -0,0 +1,281 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-21. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus + +import okhttp3.Cookie +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.currentTimeUnix +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_API +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_MESSAGES +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_PORTAL +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_SYNERGIA +import pl.szczodrzynski.edziennik.data.api.models.Data +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.isNotNullNorEmpty + +class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { + + fun isPortalLoginValid() = portalTokenExpiryTime-30 > currentTimeUnix() && portalRefreshToken.isNotNullNorEmpty() && portalAccessToken.isNotNullNorEmpty() + fun isApiLoginValid() = apiTokenExpiryTime-30 > currentTimeUnix() && apiAccessToken.isNotNullNorEmpty() + fun isSynergiaLoginValid() = synergiaSessionIdExpiryTime-30 > currentTimeUnix() && synergiaSessionId.isNotNullNorEmpty() + fun isMessagesLoginValid() = messagesSessionIdExpiryTime-30 > currentTimeUnix() && messagesSessionId.isNotNullNorEmpty() + + override fun satisfyLoginMethods() { + loginMethods.clear() + if (isPortalLoginValid()) + loginMethods += LOGIN_METHOD_LIBRUS_PORTAL + if (isApiLoginValid()) + loginMethods += LOGIN_METHOD_LIBRUS_API + if (isSynergiaLoginValid()) { + loginMethods += LOGIN_METHOD_LIBRUS_SYNERGIA + app.cookieJar.saveFromResponse(null, listOf( + Cookie.Builder() + .name("DZIENNIKSID") + .value(synergiaSessionId!!) + .domain("synergia.librus.pl") + .secure().httpOnly().build() + )) + } + if (isMessagesLoginValid()) { + loginMethods += LOGIN_METHOD_LIBRUS_MESSAGES + app.cookieJar.saveFromResponse(null, listOf( + Cookie.Builder() + .name("DZIENNIKSID") + .value(messagesSessionId!!) + .domain("wiadomosci.librus.pl") + .secure().httpOnly().build() + )) + } + } + + fun getColor(id: Int?): Int { + return when (id) { + 1 -> 0xFFF0E68C + 2 -> 0xFF87CEFA + 3 -> 0xFFB0C4DE + 4 -> 0xFFF0F8FF + 5 -> 0xFFF0FFFF + 6 -> 0xFFF5F5DC + 7 -> 0xFFFFEBCD + 8 -> 0xFFFFF8DC + 9 -> 0xFFA9A9A9 + 10 -> 0xFFBDB76B + 11 -> 0xFF8FBC8F + 12 -> 0xFFDCDCDC + 13 -> 0xFFDAA520 + 14 -> 0xFFE6E6FA + 15 -> 0xFFFFA07A + 16 -> 0xFF32CD32 + 17 -> 0xFF66CDAA + 18 -> 0xFF66CDAA + 19 -> 0xFFC0C0C0 + 20 -> 0xFFD2B48C + 21 -> 0xFF3333FF + 22 -> 0xFF7B68EE + 23 -> 0xFFBA55D3 + 24 -> 0xFFFFB6C1 + 25 -> 0xFFFF1493 + 26 -> 0xFFDC143C + 27 -> 0xFFFF0000 + 28 -> 0xFFFF8C00 + 29 -> 0xFFFFD700 + 30 -> 0xFFADFF2F + 31 -> 0xFF7CFC00 + else -> 0xff2196f3 + }.toInt() + } + + /* _____ _ _ + | __ \ | | | | + | |__) |__ _ __| |_ __ _| | + | ___/ _ \| '__| __/ _` | | + | | | (_) | | | || (_| | | + |_| \___/|_| \__\__,_|*/ + private var mPortalEmail: String? = null + var portalEmail: String? + get() { mPortalEmail = mPortalEmail ?: loginStore.getLoginData("email", null); return mPortalEmail } + set(value) { loginStore.putLoginData("email", value); mPortalEmail = value } + private var mPortalPassword: String? = null + var portalPassword: String? + get() { mPortalPassword = mPortalPassword ?: loginStore.getLoginData("password", null); return mPortalPassword } + set(value) { loginStore.putLoginData("password", value); mPortalPassword = value } + + private var mPortalAccessToken: String? = null + var portalAccessToken: String? + get() { mPortalAccessToken = mPortalAccessToken ?: loginStore.getLoginData("accessToken", null); return mPortalAccessToken } + set(value) { loginStore.putLoginData("accessToken", value); mPortalAccessToken = value } + private var mPortalRefreshToken: String? = null + var portalRefreshToken: String? + get() { mPortalRefreshToken = mPortalRefreshToken ?: loginStore.getLoginData("refreshToken", null); return mPortalRefreshToken } + set(value) { loginStore.putLoginData("refreshToken", value); mPortalRefreshToken = value } + private var mPortalTokenExpiryTime: Long? = null + var portalTokenExpiryTime: Long + get() { mPortalTokenExpiryTime = mPortalTokenExpiryTime ?: loginStore.getLoginData("tokenExpiryTime", 0L); return mPortalTokenExpiryTime ?: 0L } + set(value) { loginStore.putLoginData("tokenExpiryTime", value); mPortalTokenExpiryTime = value } + + /* _____ _____ + /\ | __ \_ _| + / \ | |__) || | + / /\ \ | ___/ | | + / ____ \| | _| |_ + /_/ \_\_| |____*/ + /** + * A Synergia login, like 1234567u. + * Used: for login (API Login Method) in Synergia mode. + * Used: for login (Synergia Login Method) in Synergia mode. + * And also in various places in [pl.szczodrzynski.edziennik.api.v2.models.Feature]s + */ + private var mApiLogin: String? = null + var apiLogin: String? + get() { mApiLogin = mApiLogin ?: profile?.getStudentData("accountLogin", null); return mApiLogin } + set(value) { profile?.putStudentData("accountLogin", value) ?: return; mApiLogin = value } + /** + * A Synergia password. + * Used: for login (API Login Method) in Synergia mode. + * Used: for login (Synergia Login Method) in Synergia mode. + */ + private var mApiPassword: String? = null + var apiPassword: String? + get() { mApiPassword = mApiPassword ?: profile?.getStudentData("accountPassword", null); return mApiPassword } + set(value) { profile?.putStudentData("accountPassword", value) ?: return; mApiPassword = value } + + /** + * A JST login Code. + * Used only during first login in JST mode. + */ + private var mApiCode: String? = null + var apiCode: String? + get() { mApiCode = mApiCode ?: loginStore.getLoginData("accountCode", null); return mApiCode } + set(value) { + loginStore.putLoginData("accountCode", value); mApiCode = value } + /** + * A JST login PIN. + * Used only during first login in JST mode. + */ + private var mApiPin: String? = null + var apiPin: String? + get() { mApiPin = mApiPin ?: loginStore.getLoginData("accountPin", null); return mApiPin } + set(value) { + loginStore.putLoginData("accountPin", value); mApiPin = value } + + /** + * A Synergia API access token. + * Used in all Api Endpoints. + * Created in Login Method Api. + * Applicable for all login modes. + */ + private var mApiAccessToken: String? = null + var apiAccessToken: String? + get() { mApiAccessToken = mApiAccessToken ?: profile?.getStudentData("accountToken", null); return mApiAccessToken } + set(value) { mApiAccessToken = value; profile?.putStudentData("accountToken", value) ?: return; } + /** + * A Synergia API refresh token. + * Used when refreshing the [apiAccessToken] in JST, Synergia modes. + */ + private var mApiRefreshToken: String? = null + var apiRefreshToken: String? + get() { mApiRefreshToken = mApiRefreshToken ?: profile?.getStudentData("accountRefreshToken", null); return mApiRefreshToken } + set(value) { mApiRefreshToken = value; profile?.putStudentData("accountRefreshToken", value) ?: return; } + /** + * The expiry time for [apiAccessToken], as a UNIX timestamp. + * Used when refreshing the [apiAccessToken] in JST, Synergia modes. + * Used when refreshing the [apiAccessToken] in Portal mode ([pl.szczodrzynski.edziennik.api.v2.librus.login.SynergiaTokenExtractor]) + */ + private var mApiTokenExpiryTime: Long? = null + var apiTokenExpiryTime: Long + get() { mApiTokenExpiryTime = mApiTokenExpiryTime ?: profile?.getStudentData("accountTokenTime", 0L); return mApiTokenExpiryTime ?: 0L } + set(value) { mApiTokenExpiryTime = value; profile?.putStudentData("accountTokenTime", value) ?: return; } + + /* _____ _ + / ____| (_) + | (___ _ _ _ __ ___ _ __ __ _ _ __ _ + \___ \| | | | '_ \ / _ \ '__/ _` | |/ _` | + ____) | |_| | | | | __/ | | (_| | | (_| | + |_____/ \__, |_| |_|\___|_| \__, |_|\__,_| + __/ | __/ | + |___/ |__*/ + /** + * A Synergia web Session ID (DZIENNIKSID). + * Used in endpoints with Synergia login method. + */ + private var mSynergiaSessionId: String? = null + var synergiaSessionId: String? + get() { mSynergiaSessionId = mSynergiaSessionId ?: profile?.getStudentData("accountSID", null); return mSynergiaSessionId } + set(value) { profile?.putStudentData("accountSID", value) ?: return; mSynergiaSessionId = value } + /** + * The expiry time for [synergiaSessionId], as a UNIX timestamp. + * Used in endpoints with Synergia login method. + * TODO verify how long is the session ID valid. + */ + private var mSynergiaSessionIdExpiryTime: Long? = null + var synergiaSessionIdExpiryTime: Long + get() { mSynergiaSessionIdExpiryTime = mSynergiaSessionIdExpiryTime ?: profile?.getStudentData("accountSIDTime", 0L); return mSynergiaSessionIdExpiryTime ?: 0L } + set(value) { profile?.putStudentData("accountSIDTime", value) ?: return; mSynergiaSessionIdExpiryTime = value } + + + /** + * A Messages web Session ID (DZIENNIKSID). + * Used in endpoints with Messages login method. + */ + private var mMessagesSessionId: String? = null + var messagesSessionId: String? + get() { mMessagesSessionId = mMessagesSessionId ?: profile?.getStudentData("messagesSID", null); return mMessagesSessionId } + set(value) { profile?.putStudentData("messagesSID", value) ?: return; mMessagesSessionId = value } + /** + * The expiry time for [messagesSessionId], as a UNIX timestamp. + * Used in endpoints with Messages login method. + * TODO verify how long is the session ID valid. + */ + private var mMessagesSessionIdExpiryTime: Long? = null + var messagesSessionIdExpiryTime: Long + get() { mMessagesSessionIdExpiryTime = mMessagesSessionIdExpiryTime ?: profile?.getStudentData("messagesSIDTime", 0L); return mMessagesSessionIdExpiryTime ?: 0L } + set(value) { profile?.putStudentData("messagesSIDTime", value) ?: return; mMessagesSessionIdExpiryTime = value } + + /* ____ _ _ + / __ \| | | | + | | | | |_| |__ ___ _ __ + | | | | __| '_ \ / _ \ '__| + | |__| | |_| | | | __/ | + \____/ \__|_| |_|\___|*/ + var isPremium + get() = profile?.getStudentData("isPremium", false) ?: false + set(value) { profile?.putStudentData("isPremium", value) } + + private var mSchoolName: String? = null + var schoolName: String? + get() { mSchoolName = mSchoolName ?: profile?.getStudentData("schoolName", null); return mSchoolName } + set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolName = value } + + private var mUnitId: Long? = null + var unitId: Long + get() { mUnitId = mUnitId ?: profile?.getStudentData("unitId", 0L); return mUnitId ?: 0L } + set(value) { profile?.putStudentData("unitId", value) ?: return; mUnitId = value } + + private var mStartPointsSemester1: Int? = null + var startPointsSemester1: Int + get() { mStartPointsSemester1 = mStartPointsSemester1 ?: profile?.getStudentData("startPointsSemester1", 0); return mStartPointsSemester1 ?: 0 } + set(value) { profile?.putStudentData("startPointsSemester1", value) ?: return; mStartPointsSemester1 = value } + + private var mStartPointsSemester2: Int? = null + var startPointsSemester2: Int + get() { mStartPointsSemester2 = mStartPointsSemester2 ?: profile?.getStudentData("startPointsSemester2", 0); return mStartPointsSemester2 ?: 0 } + set(value) { profile?.putStudentData("startPointsSemester2", value) ?: return; mStartPointsSemester2 = value } + + private var mEnablePointGrades: Boolean? = null + var enablePointGrades: Boolean + get() { mEnablePointGrades = mEnablePointGrades ?: profile?.getStudentData("enablePointGrades", true); return mEnablePointGrades ?: true } + set(value) { profile?.putStudentData("enablePointGrades", value) ?: return; mEnablePointGrades = value } + + private var mEnableDescriptiveGrades: Boolean? = null + var enableDescriptiveGrades: Boolean + get() { mEnableDescriptiveGrades = mEnableDescriptiveGrades ?: profile?.getStudentData("enableDescriptiveGrades", true); return mEnableDescriptiveGrades ?: true } + set(value) { profile?.putStudentData("enableDescriptiveGrades", value) ?: return; mEnableDescriptiveGrades = value } + + private var mTimetableNotPublic: Boolean? = null + var timetableNotPublic: Boolean + get() { mTimetableNotPublic = mTimetableNotPublic ?: profile?.getStudentData("timetableNotPublic", false); return mTimetableNotPublic ?: false } + set(value) { profile?.putStudentData("timetableNotPublic", value) ?: return; mTimetableNotPublic = value } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt new file mode 100644 index 00000000..985a4c2e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt @@ -0,0 +1,217 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-21. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusData +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api.LibrusApiAnnouncementMarkAsRead +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesGetAttachment +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesGetMessage +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesGetRecipientList +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesSendMessage +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.LibrusSynergiaMarkAllAnnouncementsAsRead +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.firstlogin.LibrusFirstLogin +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLogin +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.utils.Utils.d + +class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface { + companion object { + private const val TAG = "Librus" + } + + val internalErrorList = mutableListOf() + val data: DataLibrus + private var afterLogin: (() -> Unit)? = null + + init { + data = DataLibrus(app, profile, loginStore).apply { + callback = wrapCallback(this@Librus.callback) + satisfyLoginMethods() + } + } + + private fun completed() { + data.saveData() + data.notify { + callback.onCompleted() + } + } + + /* _______ _ _ _ _ _ + |__ __| | /\ | | (_) | | | + | | | |__ ___ / \ | | __ _ ___ _ __ _| |_| |__ _ __ ___ + | | | '_ \ / _ \ / /\ \ | |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \ + | | | | | | __/ / ____ \| | (_| | (_) | | | | |_| | | | | | | | | + |_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| + __/ | + |__*/ + override fun sync(featureIds: List, viewId: Int?, arguments: JsonObject?) { + data.arguments = arguments + data.prepare(librusLoginMethods, LibrusFeatures, featureIds, viewId) + login() + } + + private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) { + d(TAG, "Trying to login with ${data.targetLoginMethodIds}") + if (internalErrorList.isNotEmpty()) { + d(TAG, " - Internal errors:") + internalErrorList.forEach { d(TAG, " - code $it") } + } + loginMethodId?.let { data.prepareFor(librusLoginMethods, it) } + afterLogin?.let { this.afterLogin = it } + LibrusLogin(data) { + data() + } + } + + private fun data() { + d(TAG, "Endpoint IDs: ${data.targetEndpointIds}") + if (internalErrorList.isNotEmpty()) { + d(TAG, " - Internal errors:") + internalErrorList.forEach { d(TAG, " - code $it") } + } + afterLogin?.invoke() ?: LibrusData(data) { + completed() + } + } + + override fun getMessage(message: MessageFull) { + login(LOGIN_METHOD_LIBRUS_MESSAGES) { + LibrusMessagesGetMessage(data, message) { + completed() + } + } + } + + override fun sendMessage(recipients: List, subject: String, text: String) { + login(LOGIN_METHOD_LIBRUS_MESSAGES) { + LibrusMessagesSendMessage(data, recipients, subject, text) { + completed() + } + } + } + + override fun markAllAnnouncementsAsRead() { + login(LOGIN_METHOD_LIBRUS_SYNERGIA) { + LibrusSynergiaMarkAllAnnouncementsAsRead(data) { + completed() + } + } + } + + override fun getAnnouncement(announcement: AnnouncementFull) { + login(LOGIN_METHOD_LIBRUS_API) { + LibrusApiAnnouncementMarkAsRead(data, announcement) { + completed() + } + } + } + + override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) { + login(LOGIN_METHOD_LIBRUS_MESSAGES) { + LibrusMessagesGetAttachment(data, message, attachmentId, attachmentName) { + completed() + } + } + } + + override fun getRecipientList() { + login(LOGIN_METHOD_LIBRUS_MESSAGES) { + LibrusMessagesGetRecipientList(data) { + completed() + } + } + } + + override fun firstLogin() { LibrusFirstLogin(data) { completed() } } + override fun cancel() { + d(TAG, "Cancelled") + data.cancel() + } + + private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { + return object : EdziennikCallback { + override fun onCompleted() { callback.onCompleted() } + override fun onProgress(step: Float) { callback.onProgress(step) } + override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) } + override fun onError(apiError: ApiError) { + if (apiError.errorCode in internalErrorList) { + // finish immediately if the same error occurs twice during the same sync + callback.onError(apiError) + return + } + internalErrorList.add(apiError.errorCode) + when (apiError.errorCode) { + ERROR_LIBRUS_PORTAL_ACCESS_DENIED -> { + data.loginMethods.remove(LOGIN_METHOD_LIBRUS_PORTAL) + data.prepareFor(librusLoginMethods, LOGIN_METHOD_LIBRUS_PORTAL) + data.portalTokenExpiryTime = 0 + login() + } + ERROR_LIBRUS_API_ACCESS_DENIED, + ERROR_LIBRUS_API_TOKEN_EXPIRED -> { + data.loginMethods.remove(LOGIN_METHOD_LIBRUS_API) + data.prepareFor(librusLoginMethods, LOGIN_METHOD_LIBRUS_API) + data.apiTokenExpiryTime = 0 + login() + } + ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED -> { + data.loginMethods.remove(LOGIN_METHOD_LIBRUS_SYNERGIA) + data.prepareFor(librusLoginMethods, LOGIN_METHOD_LIBRUS_SYNERGIA) + data.synergiaSessionIdExpiryTime = 0 + login() + } + ERROR_LIBRUS_MESSAGES_ACCESS_DENIED -> { + data.loginMethods.remove(LOGIN_METHOD_LIBRUS_MESSAGES) + data.prepareFor(librusLoginMethods, LOGIN_METHOD_LIBRUS_MESSAGES) + data.messagesSessionIdExpiryTime = 0 + login() + } + ERROR_LOGIN_LIBRUS_PORTAL_NO_CODE, + ERROR_LOGIN_LIBRUS_PORTAL_CSRF_MISSING, + ERROR_LOGIN_LIBRUS_PORTAL_CODE_REVOKED, + ERROR_LOGIN_LIBRUS_PORTAL_CODE_EXPIRED -> { + login() + } + ERROR_LOGIN_LIBRUS_PORTAL_NO_REFRESH, + ERROR_LOGIN_LIBRUS_PORTAL_REFRESH_REVOKED, + ERROR_LOGIN_LIBRUS_PORTAL_REFRESH_INVALID -> { + data.portalRefreshToken = null + login() + } + ERROR_LOGIN_LIBRUS_SYNERGIA_TOKEN_INVALID, + ERROR_LOGIN_LIBRUS_SYNERGIA_NO_TOKEN, + ERROR_LOGIN_LIBRUS_SYNERGIA_NO_SESSION_ID -> { + login() + } + ERROR_LOGIN_LIBRUS_MESSAGES_NO_SESSION_ID -> { + login() + } + // TODO PORTAL CAPTCHA + ERROR_LIBRUS_API_TIMETABLE_NOT_PUBLIC -> { + data.timetableNotPublic = true + data() + } + ERROR_LIBRUS_API_LUCKY_NUMBER_NOT_ACTIVE, + ERROR_LIBRUS_API_NOTES_NOT_ACTIVE -> { + data() + } + else -> callback.onError(apiError) + } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/LibrusFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/LibrusFeatures.kt new file mode 100644 index 00000000..132d6af1 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/LibrusFeatures.kt @@ -0,0 +1,242 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus + +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.models.Feature + +const val ENDPOINT_LIBRUS_API_ME = 1001 +const val ENDPOINT_LIBRUS_API_SCHOOLS = 1002 +const val ENDPOINT_LIBRUS_API_CLASSES = 1003 +const val ENDPOINT_LIBRUS_API_VIRTUAL_CLASSES = 1004 +const val ENDPOINT_LIBRUS_API_UNITS = 1005 +const val ENDPOINT_LIBRUS_API_USERS = 1006 +const val ENDPOINT_LIBRUS_API_SUBJECTS = 1007 +const val ENDPOINT_LIBRUS_API_CLASSROOMS = 1008 +const val ENDPOINT_LIBRUS_API_PUSH_CONFIG = 1010 +const val ENDPOINT_LIBRUS_API_TIMETABLES = 1015 +const val ENDPOINT_LIBRUS_API_SUBSTITUTIONS = 1016 +const val ENDPOINT_LIBRUS_API_NORMAL_GRADE_CATEGORIES = 1021 +const val ENDPOINT_LIBRUS_API_POINT_GRADE_CATEGORIES = 1022 +const val ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADE_CATEGORIES = 1023 +const val ENDPOINT_LIBRUS_API_TEXT_GRADE_CATEGORIES = 1024 +const val ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GRADE_CATEGORIES = 1025 +const val ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_CATEGORIES = 1026 +const val ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_COMMENTS = 1027 +const val ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS = 1030 +const val ENDPOINT_LIBRUS_API_NORMAL_GRADES = 1031 +const val ENDPOINT_LIBRUS_API_POINT_GRADES = 1032 +const val ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES = 1033 +const val ENDPOINT_LIBRUS_API_TEXT_GRADES = 1034 +const val ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GRADES = 1035 +const val ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADES = 1036 +const val ENDPOINT_LIBRUS_API_EVENT_TYPES = 1040 +const val ENDPOINT_LIBRUS_API_EVENTS = 1041 +const val ENDPOINT_LIBRUS_API_HOMEWORK = 1050 +const val ENDPOINT_LIBRUS_API_LUCKY_NUMBER = 1060 +const val ENDPOINT_LIBRUS_API_NOTICE_TYPES = 1070 +const val ENDPOINT_LIBRUS_API_NOTICES = 1071 +const val ENDPOINT_LIBRUS_API_ATTENDANCE_TYPES = 1080 +const val ENDPOINT_LIBRUS_API_ATTENDANCES = 1081 +const val ENDPOINT_LIBRUS_API_ANNOUNCEMENTS = 1090 +const val ENDPOINT_LIBRUS_API_PT_MEETINGS = 1100 +const val ENDPOINT_LIBRUS_API_TEACHER_FREE_DAY_TYPES = 1109 +const val ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS = 1110 +const val ENDPOINT_LIBRUS_API_SCHOOL_FREE_DAYS = 1120 +const val ENDPOINT_LIBRUS_API_CLASS_FREE_DAYS = 1130 +const val ENDPOINT_LIBRUS_SYNERGIA_INFO = 2010 +const val ENDPOINT_LIBRUS_SYNERGIA_GRADES = 2020 +const val ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK = 2030 +const val ENDPOINT_LIBRUS_MESSAGES_RECEIVED = 3010 +const val ENDPOINT_LIBRUS_MESSAGES_SENT = 3020 +const val ENDPOINT_LIBRUS_MESSAGES_TRASH = 3030 +const val ENDPOINT_LIBRUS_MESSAGES_RECEIVERS = 3040 +const val ENDPOINT_LIBRUS_MESSAGES_GET = 3040 + +val LibrusFeatures = listOf( + + // push config + Feature(LOGIN_TYPE_LIBRUS, FEATURE_PUSH_CONFIG, listOf( + ENDPOINT_LIBRUS_API_PUSH_CONFIG to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)).withShouldSync { data -> + !data.app.config.sync.tokenLibrusList.contains(data.profileId) + }, + + + + + + /** + * Timetable - using API. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_TIMETABLE, listOf( + ENDPOINT_LIBRUS_API_TIMETABLES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_SUBSTITUTIONS to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)), + /** + * Agenda - using API. + * Events, Parent-teacher meetings, free days (teacher/school/class). + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_AGENDA, listOf( + ENDPOINT_LIBRUS_API_EVENTS to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_EVENT_TYPES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_PT_MEETINGS to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_TEACHER_FREE_DAY_TYPES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_SCHOOL_FREE_DAYS to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_CLASS_FREE_DAYS to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)), + /** + * Grades - using API. + * All grades + categories. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_GRADES, listOf( + ENDPOINT_LIBRUS_API_NORMAL_GRADE_CATEGORIES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_POINT_GRADE_CATEGORIES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADE_CATEGORIES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_TEXT_GRADE_CATEGORIES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GRADE_CATEGORIES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_CATEGORIES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_COMMENTS to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_NORMAL_GRADES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_POINT_GRADES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_TEXT_GRADES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GRADES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADES to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)), + /** + * Homework - using API. + * Sync only if account has premium access. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_HOMEWORK, listOf( + ENDPOINT_LIBRUS_API_HOMEWORK to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)).withShouldSync { data -> + (data as DataLibrus).isPremium + }, + /** + * Behaviour - using API. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_BEHAVIOUR, listOf( + ENDPOINT_LIBRUS_API_NOTICES to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)), + /** + * Attendance - using API. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_ATTENDANCE, listOf( + ENDPOINT_LIBRUS_API_ATTENDANCE_TYPES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_ATTENDANCES to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)), + /** + * Announcements - using API. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_ANNOUNCEMENTS, listOf( + ENDPOINT_LIBRUS_API_ANNOUNCEMENTS to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)), + + + + + + /** + * Student info - using API. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_STUDENT_INFO, listOf( + ENDPOINT_LIBRUS_API_ME to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)), + /** + * School info - using API. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_SCHOOL_INFO, listOf( + ENDPOINT_LIBRUS_API_SCHOOLS to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_UNITS to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)), + /** + * Class info - using API. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_CLASS_INFO, listOf( + ENDPOINT_LIBRUS_API_CLASSES to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)), + /** + * Team info - using API. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_TEAM_INFO, listOf( + ENDPOINT_LIBRUS_API_VIRTUAL_CLASSES to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)), + /** + * Lucky number - using API. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_LUCKY_NUMBER, listOf( + ENDPOINT_LIBRUS_API_LUCKY_NUMBER to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)).withShouldSync { data -> data.shouldSyncLuckyNumber() }, + /** + * Teacher list - using API. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_TEACHERS, listOf( + ENDPOINT_LIBRUS_API_USERS to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)), + /** + * Subject list - using API. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_SUBJECTS, listOf( + ENDPOINT_LIBRUS_API_SUBJECTS to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)), + /** + * Classroom list - using API. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_CLASSROOMS, listOf( + ENDPOINT_LIBRUS_API_CLASSROOMS to LOGIN_METHOD_LIBRUS_API + ), listOf(LOGIN_METHOD_LIBRUS_API)), + + /** + * Student info - using synergia scrapper. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_STUDENT_INFO, listOf( + ENDPOINT_LIBRUS_SYNERGIA_INFO to LOGIN_METHOD_LIBRUS_SYNERGIA + ), listOf(LOGIN_METHOD_LIBRUS_SYNERGIA)), + /** + * Student number - using synergia scrapper. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_STUDENT_NUMBER, listOf( + ENDPOINT_LIBRUS_SYNERGIA_INFO to LOGIN_METHOD_LIBRUS_SYNERGIA + ), listOf(LOGIN_METHOD_LIBRUS_SYNERGIA)), + + + /** + * Grades - using API + synergia scrapper. + */ + /*Feature(LOGIN_TYPE_LIBRUS, FEATURE_GRADES, listOf( + ENDPOINT_LIBRUS_API_NORMAL_GC to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_API_NORMAL_GRADES to LOGIN_METHOD_LIBRUS_API, + ENDPOINT_LIBRUS_SYNERGIA_GRADES to LOGIN_METHOD_LIBRUS_SYNERGIA + ), listOf(LOGIN_METHOD_LIBRUS_API, LOGIN_METHOD_LIBRUS_SYNERGIA)),*/ + /*Endpoint(LOGIN_TYPE_LIBRUS, FEATURE_GRADES, listOf( + ENDPOINT_LIBRUS_SYNERGIA_GRADES to LOGIN_METHOD_LIBRUS_SYNERGIA + ), listOf(LOGIN_METHOD_LIBRUS_SYNERGIA)),*/ + + /** + * Homework - using scrapper. + * Sync only if account has not premium access. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_HOMEWORK, listOf( + ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK to LOGIN_METHOD_LIBRUS_SYNERGIA + ), listOf(LOGIN_METHOD_LIBRUS_SYNERGIA)).withShouldSync { data -> + !(data as DataLibrus).isPremium + }, + + /** + * Messages inbox - using messages website. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_MESSAGES_INBOX, listOf( + ENDPOINT_LIBRUS_MESSAGES_RECEIVED to LOGIN_METHOD_LIBRUS_MESSAGES + ), listOf(LOGIN_METHOD_LIBRUS_MESSAGES)), + /** + * Messages sent - using messages website. + */ + Feature(LOGIN_TYPE_LIBRUS, FEATURE_MESSAGES_SENT, listOf( + ENDPOINT_LIBRUS_MESSAGES_SENT to LOGIN_METHOD_LIBRUS_MESSAGES + ), listOf(LOGIN_METHOD_LIBRUS_MESSAGES)) +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusApi.kt new file mode 100644 index 00000000..317a79aa --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusApi.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-21. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data + +import com.google.gson.JsonObject +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.callback.JsonCallbackHandler +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.net.HttpURLConnection.* + +open class LibrusApi(open val data: DataLibrus) { + companion object { + private const val TAG = "LibrusApi" + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + fun apiGet(tag: String, endpoint: String, method: Int = GET, payload: JsonObject? = null, onSuccess: (json: JsonObject) -> Unit) { + + d(tag, "Request: Librus/Api - ${if (data.fakeLogin) FAKE_LIBRUS_API else LIBRUS_API_URL}/$endpoint") + + val callback = object : JsonCallbackHandler() { + override fun onSuccess(json: JsonObject?, response: Response?) { + if (response?.code() == HTTP_UNAVAILABLE) { + data.error(ApiError(tag, ERROR_LIBRUS_API_MAINTENANCE) + .withApiResponse(json) + .withResponse(response)) + return + } + + if (json == null && response?.parserErrorBody == null) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + val error = if (response?.code() == 200) null else + json.getString("Code") ?: + json.getString("Message") ?: + response?.parserErrorBody + error?.let { code -> + when (code) { + "TokenIsExpired" -> ERROR_LIBRUS_API_TOKEN_EXPIRED + "Insufficient scopes" -> ERROR_LIBRUS_API_INSUFFICIENT_SCOPES + "Request is denied" -> ERROR_LIBRUS_API_ACCESS_DENIED + "Resource not found" -> ERROR_LIBRUS_API_RESOURCE_NOT_FOUND + "NotFound" -> ERROR_LIBRUS_API_DATA_NOT_FOUND + "AccessDeny" -> when (json.getString("Message")) { + "Student timetable is not public" -> ERROR_LIBRUS_API_TIMETABLE_NOT_PUBLIC + else -> ERROR_LIBRUS_API_RESOURCE_ACCESS_DENIED + } + "LuckyNumberIsNotActive" -> ERROR_LIBRUS_API_LUCKY_NUMBER_NOT_ACTIVE + "NotesIsNotActive" -> ERROR_LIBRUS_API_NOTES_NOT_ACTIVE + "InvalidRequest" -> ERROR_LIBRUS_API_INVALID_REQUEST_PARAMS + "Nieprawidłowy węzeł." -> ERROR_LIBRUS_API_INCORRECT_ENDPOINT + else -> ERROR_LIBRUS_API_OTHER + }.let { errorCode -> + data.error(ApiError(tag, errorCode) + .withApiResponse(json) + .withResponse(response)) + return + } + } + + if (json == null) { + data.error(ApiError(tag, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + try { + onSuccess(json) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_LIBRUS_API_REQUEST) + .withResponse(response) + .withThrowable(e) + .withApiResponse(json)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + // TODO add hotfix for Classrooms 500 + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + Request.builder() + .url("${if (data.fakeLogin) FAKE_LIBRUS_API else LIBRUS_API_URL}/$endpoint") + .userAgent(LIBRUS_USER_AGENT) + .addHeader("Authorization", "Bearer ${data.apiAccessToken}") + .apply { + when (method) { + GET -> get() + POST -> post() + } + if (payload != null) + setJsonBody(payload) + } + .allowErrorCode(HTTP_BAD_REQUEST) + .allowErrorCode(HTTP_FORBIDDEN) + .allowErrorCode(HTTP_UNAUTHORIZED) + .allowErrorCode(HTTP_UNAVAILABLE) + .callback(callback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusData.kt new file mode 100644 index 00000000..42b33c13 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusData.kt @@ -0,0 +1,211 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesGetList +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.LibrusSynergiaHomework +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.LibrusSynergiaInfo +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.utils.Utils + +class LibrusData(val data: DataLibrus, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "LibrusEndpoints" + } + + init { + nextEndpoint(onSuccess) + } + + private fun nextEndpoint(onSuccess: () -> Unit) { + if (data.targetEndpointIds.isEmpty()) { + onSuccess() + return + } + if (data.cancelled) { + onSuccess() + return + } + useEndpoint(data.targetEndpointIds.removeAt(0)) { + data.progress(data.progressStep) + nextEndpoint(onSuccess) + } + } + + private fun useEndpoint(endpointId: Int, onSuccess: () -> Unit) { + Utils.d(TAG, "Using endpoint $endpointId") + when (endpointId) { + /** + * API + */ + ENDPOINT_LIBRUS_API_ME -> { + data.startProgress(R.string.edziennik_progress_endpoint_student_info) + LibrusApiMe(data, onSuccess) + } + ENDPOINT_LIBRUS_API_SCHOOLS -> { + data.startProgress(R.string.edziennik_progress_endpoint_school_info) + LibrusApiSchools(data, onSuccess) + } + ENDPOINT_LIBRUS_API_CLASSES -> { + data.startProgress(R.string.edziennik_progress_endpoint_classes) + LibrusApiClasses(data, onSuccess) + } + ENDPOINT_LIBRUS_API_VIRTUAL_CLASSES -> { + data.startProgress(R.string.edziennik_progress_endpoint_teams) + LibrusApiVirtualClasses(data, onSuccess) + } + ENDPOINT_LIBRUS_API_UNITS -> { + data.startProgress(R.string.edziennik_progress_endpoint_units) + LibrusApiUnits(data, onSuccess) + } + ENDPOINT_LIBRUS_API_USERS -> { + data.startProgress(R.string.edziennik_progress_endpoint_teachers) + LibrusApiUsers(data, onSuccess) + } + ENDPOINT_LIBRUS_API_SUBJECTS -> { + data.startProgress(R.string.edziennik_progress_endpoint_subjects) + LibrusApiSubjects(data, onSuccess) + } + ENDPOINT_LIBRUS_API_CLASSROOMS -> { + data.startProgress(R.string.edziennik_progress_endpoint_classrooms) + LibrusApiClassrooms(data, onSuccess) + } + // TODO push config + ENDPOINT_LIBRUS_API_TIMETABLES -> { + data.startProgress(R.string.edziennik_progress_endpoint_timetable) + LibrusApiTimetables(data, onSuccess) + } + + ENDPOINT_LIBRUS_API_NORMAL_GRADE_CATEGORIES -> { + data.startProgress(R.string.edziennik_progress_endpoint_grade_categories) + LibrusApiGradeCategories(data, onSuccess) + } + ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_CATEGORIES -> { + data.startProgress(R.string.edziennik_progress_endpoint_grade_categories) + LibrusApiBehaviourGradeCategories(data, onSuccess) + } + ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADE_CATEGORIES -> { + data.startProgress(R.string.edziennik_progress_endpoint_grade_categories) + LibrusApiDescriptiveGradeCategories(data, onSuccess) + } + ENDPOINT_LIBRUS_API_TEXT_GRADE_CATEGORIES -> { + data.startProgress(R.string.edziennik_progress_endpoint_grade_categories) + LibrusApiTextGradeCategories(data, onSuccess) + } + ENDPOINT_LIBRUS_API_POINT_GRADE_CATEGORIES -> { + data.startProgress(R.string.edziennik_progress_endpoint_grade_categories) + LibrusApiPointGradeCategories(data, onSuccess) + } + + ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS -> { + data.startProgress(R.string.edziennik_progress_endpoint_grade_comments) + LibrusApiGradeComments(data, onSuccess) + } + ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_COMMENTS -> { + data.startProgress(R.string.edziennik_progress_endpoint_grade_comments) + LibrusApiBehaviourGradeComments(data, onSuccess) + } + + ENDPOINT_LIBRUS_API_NORMAL_GRADES -> { + data.startProgress(R.string.edziennik_progress_endpoint_grades) + LibrusApiGrades(data, onSuccess) + } + ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADES -> { + data.startProgress(R.string.edziennik_progress_endpoint_behaviour_grades) + LibrusApiBehaviourGrades(data, onSuccess) + } + ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES -> { + data.startProgress(R.string.edziennik_progress_endpoint_descriptive_grades) + LibrusApiDescriptiveGrades(data, onSuccess) + } + ENDPOINT_LIBRUS_API_TEXT_GRADES -> { + data.startProgress(R.string.edziennik_progress_endpoint_descriptive_grades) + LibrusApiTextGrades(data, onSuccess) + } + ENDPOINT_LIBRUS_API_POINT_GRADES -> { + data.startProgress(R.string.edziennik_progress_endpoint_point_grades) + LibrusApiPointGrades(data, onSuccess) + } + + ENDPOINT_LIBRUS_API_EVENT_TYPES -> { + data.startProgress(R.string.edziennik_progress_endpoint_event_types) + LibrusApiEventTypes(data, onSuccess) + } + ENDPOINT_LIBRUS_API_EVENTS -> { + data.startProgress(R.string.edziennik_progress_endpoint_events) + LibrusApiEvents(data, onSuccess) + } + ENDPOINT_LIBRUS_API_HOMEWORK -> { + data.startProgress(R.string.edziennik_progress_endpoint_homework) + LibrusApiHomework(data, onSuccess) + } + ENDPOINT_LIBRUS_API_LUCKY_NUMBER -> { + data.startProgress(R.string.edziennik_progress_endpoint_lucky_number) + LibrusApiLuckyNumber(data, onSuccess) + } + ENDPOINT_LIBRUS_API_NOTICE_TYPES -> { + data.startProgress(R.string.edziennik_progress_endpoint_notice_types) + LibrusApiNoticeTypes(data, onSuccess) + } + ENDPOINT_LIBRUS_API_NOTICES -> { + data.startProgress(R.string.edziennik_progress_endpoint_notices) + LibrusApiNotices(data, onSuccess) + } + ENDPOINT_LIBRUS_API_ATTENDANCE_TYPES -> { + data.startProgress(R.string.edziennik_progress_endpoint_attendance_types) + LibrusApiAttendanceTypes(data, onSuccess) + } + ENDPOINT_LIBRUS_API_ATTENDANCES -> { + data.startProgress(R.string.edziennik_progress_endpoint_attendance) + LibrusApiAttendances(data, onSuccess) + } + ENDPOINT_LIBRUS_API_ANNOUNCEMENTS -> { + data.startProgress(R.string.edziennik_progress_endpoint_announcements) + LibrusApiAnnouncements(data, onSuccess) + } + ENDPOINT_LIBRUS_API_PT_MEETINGS -> { + data.startProgress(R.string.edziennik_progress_endpoint_pt_meetings) + LibrusApiPtMeetings(data, onSuccess) + } + ENDPOINT_LIBRUS_API_TEACHER_FREE_DAY_TYPES -> { + data.startProgress(R.string.edziennik_progress_endpoint_teacher_free_day_types) + LibrusApiTeacherFreeDayTypes(data, onSuccess) + } + ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS -> { + data.startProgress(R.string.edziennik_progress_endpoint_teacher_free_days) + LibrusApiTeacherFreeDays(data, onSuccess) + } + + /** + * SYNERGIA + */ + ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK -> { + data.startProgress(R.string.edziennik_progress_endpoint_homework) + LibrusSynergiaHomework(data, onSuccess) + } + ENDPOINT_LIBRUS_SYNERGIA_INFO -> { + data.startProgress(R.string.edziennik_progress_endpoint_student_info) + LibrusSynergiaInfo(data, onSuccess) + } + + /** + * MESSAGES + */ + ENDPOINT_LIBRUS_MESSAGES_RECEIVED -> { + data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox) + LibrusMessagesGetList(data, type = Message.TYPE_RECEIVED, onSuccess = onSuccess) + } + ENDPOINT_LIBRUS_MESSAGES_SENT -> { + data.startProgress(R.string.edziennik_progress_endpoint_messages_outbox) + LibrusMessagesGetList(data, type = Message.TYPE_SENT, onSuccess = onSuccess) + } + + else -> onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusMessages.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusMessages.kt new file mode 100644 index 00000000..01813fa3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusMessages.kt @@ -0,0 +1,302 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-24 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data + +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.body.MediaTypeUtils +import im.wangchao.mhttp.callback.FileCallbackHandler +import im.wangchao.mhttp.callback.JsonCallbackHandler +import im.wangchao.mhttp.callback.TextCallbackHandler +import okhttp3.Cookie +import org.json.JSONObject +import org.json.XML +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.parser.Parser +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.io.File +import java.io.StringWriter +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + +open class LibrusMessages(open val data: DataLibrus) { + companion object { + private const val TAG = "LibrusMessages" + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + fun messagesGet(tag: String, module: String, method: Int = POST, + parameters: Map? = null, onSuccess: (doc: Document) -> Unit) { + + d(tag, "Request: Librus/Messages - $LIBRUS_MESSAGES_URL/$module") + + val callback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + if (text.isNullOrEmpty()) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + when { + text.contains("Niepoprawny login i/lub hasło.") -> data.error(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN, response, text) + text.contains("stop.png") -> data.error(TAG, ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED, response, text) + text.contains("eAccessDeny") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text) + text.contains("OffLine") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_MAINTENANCE, response, text) + text.contains("error") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ERROR, response, text) + text.contains("eVarWhitThisNameNotExists") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text) + text.contains("") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_OTHER, response, text) + } + + try { + val doc = Jsoup.parse(text, "", Parser.xmlParser()) + onSuccess(doc) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_LIBRUS_MESSAGES_REQUEST) + .withResponse(response) + .withThrowable(e) + .withApiResponse(text)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + data.app.cookieJar.saveFromResponse(null, listOf( + Cookie.Builder() + .name("DZIENNIKSID") + .value(data.messagesSessionId!!) + .domain("wiadomosci.librus.pl") + .secure().httpOnly().build() + )) + + + val docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder() + val doc = docBuilder.newDocument() + val serviceElement = doc.createElement("service") + val headerElement = doc.createElement("header") + val dataElement = doc.createElement("data") + for ((key, value) in parameters.orEmpty()) { + val element = doc.createElement(key) + element.appendChild(doc.createTextNode(value.toString())) + dataElement.appendChild(element) + } + serviceElement.appendChild(headerElement) + serviceElement.appendChild(dataElement) + doc.appendChild(serviceElement) + val transformer = TransformerFactory.newInstance().newTransformer() + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes") + val stringWriter = StringWriter() + transformer.transform(DOMSource(doc), StreamResult(stringWriter)) + val requestXml = stringWriter.toString() + + Request.builder() + .url("$LIBRUS_MESSAGES_URL/$module") + .userAgent(SYNERGIA_USER_AGENT) + .setTextBody(requestXml, MediaTypeUtils.APPLICATION_XML) + .apply { + when (method) { + GET -> get() + POST -> post() + } + } + .callback(callback) + .build() + .enqueue() + } + + fun messagesGetJson(tag: String, module: String, method: Int = POST, + parameters: Map? = null, onSuccess: (json: JsonObject?) -> Unit) { + + d(tag, "Request: Librus/Messages - $LIBRUS_MESSAGES_URL/$module") + + val callback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + if (text.isNullOrEmpty()) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + when { + text.contains("Niepoprawny login i/lub hasło.") -> data.error(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN, response, text) + text.contains("stop.png") -> data.error(TAG, ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED, response, text) + text.contains("eAccessDeny") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text) + text.contains("OffLine") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_MAINTENANCE, response, text) + text.contains("error") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ERROR, response, text) + text.contains("eVarWhitThisNameNotExists") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text) + text.contains("") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_OTHER, response, text) + } + + try { + val json: JSONObject? = XML.toJSONObject(text) + onSuccess(JsonParser().parse(json?.toString() ?: "{}")?.asJsonObject) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_LIBRUS_MESSAGES_REQUEST) + .withResponse(response) + .withThrowable(e) + .withApiResponse(text)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + data.app.cookieJar.saveFromResponse(null, listOf( + Cookie.Builder() + .name("DZIENNIKSID") + .value(data.messagesSessionId!!) + .domain("wiadomosci.librus.pl") + .secure().httpOnly().build() + )) + + + val docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder() + val doc = docBuilder.newDocument() + val serviceElement = doc.createElement("service") + val headerElement = doc.createElement("header") + val dataElement = doc.createElement("data") + for ((key, value) in parameters.orEmpty()) { + val element = doc.createElement(key) + element.appendChild(doc.createTextNode(value.toString())) + dataElement.appendChild(element) + } + serviceElement.appendChild(headerElement) + serviceElement.appendChild(dataElement) + doc.appendChild(serviceElement) + val transformer = TransformerFactory.newInstance().newTransformer() + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes") + val stringWriter = StringWriter() + transformer.transform(DOMSource(doc), StreamResult(stringWriter)) + val requestXml = stringWriter.toString() + + Request.builder() + .url("$LIBRUS_MESSAGES_URL/$module") + .userAgent(SYNERGIA_USER_AGENT) + .setTextBody(requestXml, MediaTypeUtils.APPLICATION_XML) + .apply { + when (method) { + GET -> get() + POST -> post() + } + } + .callback(callback) + .build() + .enqueue() + } + + fun sandboxGet(tag: String, action: String, parameters: Map? = null, + onSuccess: (json: JsonObject) -> Unit) { + + d(tag, "Request: Librus/Messages - $LIBRUS_SANDBOX_URL$action") + + val callback = object : JsonCallbackHandler() { + override fun onSuccess(json: JsonObject?, response: Response?) { + if (json == null) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + try { + onSuccess(json) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_LIBRUS_MESSAGES_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("$LIBRUS_SANDBOX_URL$action") + .userAgent(SYNERGIA_USER_AGENT) + .apply { + parameters?.forEach { (k, v) -> + addParameter(k, v) + } + } + .post() + .callback(callback) + .build() + .enqueue() + } + + fun sandboxGetFile(tag: String, action: String, targetFile: File, onSuccess: (file: File) -> Unit, + onProgress: (written: Long, total: Long) -> Unit) { + + d(tag, "Request: Librus/Messages - $LIBRUS_SANDBOX_URL$action") + + val callback = object : FileCallbackHandler(targetFile) { + override fun onSuccess(file: File?, response: Response?) { + if (file == null) { + data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD) + .withResponse(response)) + return + } + + try { + onSuccess(file) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_LIBRUS_MESSAGES_FILE_REQUEST) + .withResponse(response) + .withThrowable(e)) + } + } + + override fun onProgress(bytesWritten: Long, bytesTotal: Long) { + try { + onProgress(bytesWritten, bytesTotal) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_LIBRUS_MESSAGES_FILE_REQUEST) + .withThrowable(e)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + Request.builder() + .url("$LIBRUS_SANDBOX_URL$action") + .userAgent(SYNERGIA_USER_AGENT) + .post() + .callback(callback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusPortal.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusPortal.kt new file mode 100644 index 00000000..48017438 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusPortal.kt @@ -0,0 +1,104 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data + +import com.google.gson.JsonObject +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.callback.JsonCallbackHandler +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.net.HttpURLConnection + +open class LibrusPortal(open val data: DataLibrus) { + companion object { + private const val TAG = "LibrusPortal" + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + fun portalGet(tag: String, endpoint: String, method: Int = GET, payload: JsonObject? = null, onSuccess: (json: JsonObject, response: Response?) -> Unit) { + + d(tag, "Request: Librus/Portal - ${if (data.fakeLogin) FAKE_LIBRUS_PORTAL else LIBRUS_PORTAL_URL}$endpoint") + + 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 error = if (response?.code() == 200) null else + json.getString("reason") ?: + json.getString("message") ?: + json.getString("hint") ?: + json.getString("Code") + error?.let { code -> + when (code) { + "requires_an_action" -> ERROR_LIBRUS_PORTAL_SYNERGIA_DISCONNECTED + "Access token is invalid" -> ERROR_LIBRUS_PORTAL_ACCESS_DENIED + "ApiDisabled" -> ERROR_LIBRUS_PORTAL_API_DISABLED + "Account not found" -> ERROR_LIBRUS_PORTAL_SYNERGIA_NOT_FOUND + else -> when (json.getString("hint")) { + "Error while decoding to JSON" -> ERROR_LIBRUS_PORTAL_ACCESS_DENIED + else -> ERROR_LIBRUS_PORTAL_OTHER + } + }.let { errorCode -> + data.error(ApiError(tag, errorCode) + .withApiResponse(json) + .withResponse(response)) + return + } + } + if (response?.code() == HttpURLConnection.HTTP_OK) { + try { + onSuccess(json, response) + } catch (e: NullPointerException) { + e.printStackTrace() + data.error(ApiError(tag, EXCEPTION_LIBRUS_PORTAL_SYNERGIA_TOKEN) + .withResponse(response) + .withThrowable(e) + .withApiResponse(json)) + } + + } else { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withApiResponse(json)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + Request.builder() + .url((if (data.fakeLogin) FAKE_LIBRUS_PORTAL else LIBRUS_PORTAL_URL) + endpoint) + .userAgent(LIBRUS_USER_AGENT) + .addHeader("Authorization", "Bearer ${data.portalAccessToken}") + .apply { + when (method) { + GET -> get() + POST -> post() + } + if (payload != null) + setJsonBody(payload) + } + .allowErrorCode(HttpURLConnection.HTTP_NOT_FOUND) + .allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN) + .allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED) + .allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST) + .allowErrorCode(HttpURLConnection.HTTP_GONE) + .callback(callback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusSynergia.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusSynergia.kt new file mode 100644 index 00000000..d748cb13 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusSynergia.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-21. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data + +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.callback.TextCallbackHandler +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.utils.Utils.d + +open class LibrusSynergia(open val data: DataLibrus) { + companion object { + private const val TAG = "LibrusSynergia" + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + fun synergiaGet(tag: String, endpoint: String, method: Int = GET, + parameters: Map = emptyMap(), onSuccess: (text: String) -> Unit) { + d(tag, "Request: Librus/Synergia - $LIBRUS_SYNERGIA_URL/$endpoint") + + val callback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + if (text.isNullOrEmpty()) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + if (!text.contains("jesteś zalogowany")) { + when { + text.contains("stop.png") -> ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED + text.contains("Przerwa techniczna") -> ERROR_LIBRUS_SYNERGIA_MAINTENANCE + else -> ERROR_LIBRUS_SYNERGIA_OTHER + }.let { errorCode -> + data.error(ApiError(tag, errorCode) + .withResponse(response) + .withApiResponse(text)) + return + } + } + + + try { + onSuccess(text) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_LIBRUS_SYNERGIA_REQUEST) + .withResponse(response) + .withThrowable(e) + .withApiResponse(text)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + /*data.app.cookieJar.saveFromResponse(null, listOf( + Cookie.Builder() + .name("DZIENNIKSID") + .value(data.synergiaSessionId!!) + .domain("synergia.librus.pl") + .secure().httpOnly().build() + ))*/ + + Request.builder() + .url("$LIBRUS_SYNERGIA_URL/$endpoint") + .userAgent(LIBRUS_USER_AGENT) + .apply { + when (method) { + GET -> get() + POST -> post() + } + parameters.map { (name, value) -> + addParameter(name, value) + } + } + .callback(callback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAnnouncementMarkAsRead.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAnnouncementMarkAsRead.kt new file mode 100644 index 00000000..9e08631c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAnnouncementMarkAsRead.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-27 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.POST +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.api.events.AnnouncementGetEvent +import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata + +class LibrusApiAnnouncementMarkAsRead( + override val data: DataLibrus, + private val announcement: AnnouncementFull, + val onSuccess: () -> Unit +) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiAnnouncementMarkAsRead" + } + + init { + apiGet(TAG, "SchoolNotices/MarkAsRead/${announcement.idString}", method = POST) { + announcement.seen = true + + EventBus.getDefault().postSticky(AnnouncementGetEvent(announcement)) + + data.setSeenMetadataList.add(Metadata( + profileId, + Metadata.TYPE_ANNOUNCEMENT, + announcement.id, + announcement.seen, + announcement.notified, + announcement.addedDate + )) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAnnouncements.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAnnouncements.kt new file mode 100644 index 00000000..0236fcc9 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAnnouncements.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-13 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_ANNOUNCEMENTS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.announcements.Announcement +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date + +class LibrusApiAnnouncements(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiAnnouncements" + } + + init { data.profile?.also { profile -> + apiGet(TAG, "SchoolNotices") { json -> + val announcements = json.getJsonArray("SchoolNotices").asJsonObjectList() + + announcements?.forEach { announcement -> + val longId = announcement.getString("Id") ?: return@forEach + val id = longId.crc32() + val subject = announcement.getString("Subject") ?: "" + val text = announcement.getString("Content") ?: "" + val startDate = Date.fromY_m_d(announcement.getString("StartDate")) + val endDate = Date.fromY_m_d(announcement.getString("EndDate")) + val teacherId = announcement.getJsonObject("AddedBy")?.getLong("Id") ?: -1 + val addedDate = announcement.getString("CreationDate")?.let { Date.fromIso(it) } + ?: System.currentTimeMillis() + val read = announcement.getBoolean("WasRead") ?: false + + val announcementObject = Announcement( + profileId, + id, + subject, + text, + startDate, + endDate, + teacherId, + longId + ) + + data.announcementList.add(announcementObject) + data.setSeenMetadataList.add(Metadata( + profileId, + Metadata.TYPE_ANNOUNCEMENT, + id, + read, + profile.empty || read, + addedDate + )) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_ANNOUNCEMENTS, SYNC_ALWAYS) + onSuccess() + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAttendanceTypes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAttendanceTypes.kt new file mode 100644 index 00000000..2d85e1fa --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAttendanceTypes.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-13 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import android.graphics.Color +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_ATTENDANCE_TYPES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance +import pl.szczodrzynski.edziennik.data.db.modules.attendance.AttendanceType + +class LibrusApiAttendanceTypes(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiAttendanceTypes" + } + + init { + apiGet(TAG, "Attendances/Types") { json -> + val attendanceTypes = json.getJsonArray("Types").asJsonObjectList() + + attendanceTypes?.forEach { attendanceType -> + val id = attendanceType.getLong("Id") ?: return@forEach + val name = attendanceType.getString("Name") ?: "" + val color = attendanceType.getString("ColorRGB")?.let { Color.parseColor("#$it") } ?: -1 + + val standardId = when (attendanceType.getBoolean("Standard") ?: false) { + true -> id + false -> attendanceType.getJsonObject("StandardType")?.getLong("Id") ?: id + } + val type = when (standardId) { + 1L -> Attendance.TYPE_ABSENT + 2L -> Attendance.TYPE_BELATED + 3L -> Attendance.TYPE_ABSENT_EXCUSED + 4L -> Attendance.TYPE_RELEASED + /*100*/else -> Attendance.TYPE_PRESENT + } + + data.attendanceTypes.put(id, AttendanceType(profileId, id, name, type, color)) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_ATTENDANCE_TYPES, 4*DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAttendances.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAttendances.kt new file mode 100644 index 00000000..bbc0d26e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAttendances.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-13 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import androidx.core.util.isEmpty +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_ATTENDANCES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.models.Date + +class LibrusApiAttendances(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiAttendances" + } + + init { + if (data.attendanceTypes.isEmpty()) { + data.db.attendanceTypeDao().getAllNow(profileId).toSparseArray(data.attendanceTypes) { it.id } + } + + apiGet(TAG, "Attendances") { json -> + val attendances = json.getJsonArray("Attendances").asJsonObjectList() + + attendances?.forEach { attendance -> + val id = Utils.strToInt((attendance.getString("Id") ?: return@forEach) + .replace("[^\\d.]".toRegex(), "")).toLong() + val teacherId = attendance.getJsonObject("AddedBy")?.getLong("Id") ?: -1 + val lessonNo = attendance.getInt("LessonNo") ?: return@forEach + val startTime = data.lessonRanges.get(lessonNo).startTime + val lessonDate = Date.fromY_m_d(attendance.getString("Date")) + val semester = attendance.getInt("Semester") ?: return@forEach + val type = attendance.getJsonObject("Type")?.getLong("Id") ?: return@forEach + val typeObject = data.attendanceTypes.get(type) + val topic = typeObject?.name ?: "" + + val lessonList = data.db.timetableDao().getForDateNow(profileId, lessonDate) + val subjectId = lessonList.firstOrNull { it.startTime == startTime }?.subjectId ?: -1 + + val attendanceObject = Attendance( + profileId, + id, + teacherId, + subjectId, + semester, + topic, + lessonDate, + startTime, + typeObject.type + ) + + val addedDate = Date.fromIso(attendance.getString("AddDate") ?: return@forEach) + + data.attendanceList.add(attendanceObject) + if(typeObject.type != Attendance.TYPE_PRESENT) { + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_ATTENDANCE, + id, + profile?.empty ?: false, + profile?.empty ?: false, + addedDate + )) + } + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_ATTENDANCES, SYNC_ALWAYS) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGradeCategories.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGradeCategories.kt new file mode 100644 index 00000000..c12499ef --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGradeCategories.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-3 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import android.graphics.Color +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_CATEGORIES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory + +class LibrusApiBehaviourGradeCategories(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiBehaviourGradeCategories" + } + + init { + apiGet(TAG, "BehaviourGrades/Points/Categories") { json -> + json.getJsonArray("Categories")?.asJsonObjectList()?.forEach { category -> + val id = category.getLong("Id") ?: return@forEach + val name = category.getString("Name") ?: "" + val valueFrom = category.getFloat("ValueFrom") ?: 0f + val valueTo = category.getFloat("ValueTo") ?: 0f + + val gradeCategoryObject = GradeCategory( + profileId, + id, + -1f, + Color.BLUE, + name + ).apply { + type = GradeCategory.TYPE_BEHAVIOUR + setValueRange(valueFrom, valueTo) + } + + data.gradeCategories.put(id, gradeCategoryObject) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_CATEGORIES, 1 * WEEK) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGradeComments.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGradeComments.kt new file mode 100644 index 00000000..43a4d5f9 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGradeComments.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-7 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_COMMENTS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory + +class LibrusApiBehaviourGradeComments(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiBehaviourGradeComments" + } + + init { + apiGet(TAG, "BehaviourGrades/Points/Comments") { json -> + + json.getJsonArray("Comments")?.asJsonObjectList()?.forEach { comment -> + val id = comment.getLong("Id") ?: return@forEach + val text = comment.getString("Text")?.fixWhiteSpaces() ?: return@forEach + + val gradeCategoryObject = GradeCategory( + profileId, + id, + -1f, + -1, + text + ).apply { + type = GradeCategory.TYPE_BEHAVIOUR_COMMENT + } + + data.gradeCategories.put(id, gradeCategoryObject) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_COMMENTS, SYNC_ALWAYS) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGrades.kt new file mode 100644 index 00000000..3aa98bab --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGrades.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-3 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date +import java.text.DecimalFormat + +class LibrusApiBehaviourGrades(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiBehaviourGrades" + } + + private val nameFormat by lazy { DecimalFormat("#.##") } + + init { data.profile?.also { profile -> + apiGet(TAG, "BehaviourGrades/Points") { json -> + + if (data.startPointsSemester1 > 0) { + val semester1StartGradeObject = Grade( + profileId, + -101, + data.app.getString(R.string.grade_start_points), + 0xffbdbdbd.toInt(), + data.app.getString(R.string.grade_start_points_format, 1), + nameFormat.format(data.startPointsSemester1), + data.startPointsSemester1.toFloat(), + -1f, + 1, + -1, + 1 + ).apply { type = Grade.TYPE_POINT_SUM } + + data.gradeList.add(semester1StartGradeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_GRADE, + semester1StartGradeObject.id, + true, + true, + profile.getSemesterStart(1).inMillis + )) + } + + if (data.startPointsSemester2 > 0) { + val semester2StartGradeObject = Grade( + profileId, + -102, + data.app.getString(R.string.grade_start_points), + 0xffbdbdbd.toInt(), + data.app.getString(R.string.grade_start_points_format, 2), + nameFormat.format(data.startPointsSemester2), + data.startPointsSemester2.toFloat(), + -1f, + 2, + -1, + 1 + ).apply { type = Grade.TYPE_POINT_SUM } + + data.gradeList.add(semester2StartGradeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_GRADE, + semester2StartGradeObject.id, + true, + true, + profile.getSemesterStart(2).inMillis + )) + } + + json.getJsonArray("Grades")?.asJsonObjectList()?.forEach { grade -> + val id = grade.getLong("Id") ?: return@forEach + val value = grade.getFloat("Value") + val shortName = grade.getString("ShortName") + val semester = grade.getInt("Semester") ?: profile.currentSemester + val teacherId = grade.getJsonObject("AddedBy")?.getLong("Id") ?: -1 + val addedDate = grade.getString("AddDate")?.let { Date.fromIso(it) } + ?: System.currentTimeMillis() + + val name = when { + value != null -> (if (value >= 0) "+" else "") + nameFormat.format(value) + shortName != null -> shortName + else -> return@forEach + } + + val color = data.getColor(when { + value == null || value == 0f -> 12 + value > 0 -> 16 + value < 0 -> 26 + else -> 12 + }) + + val categoryId = grade.getJsonObject("Category")?.getLong("Id") ?: -1 + val category = data.gradeCategories.singleOrNull { + it.categoryId == categoryId && it.type == GradeCategory.TYPE_BEHAVIOUR + } + + val categoryName = category?.text ?: "" + + val description = grade.getJsonArray("Comments")?.asJsonObjectList()?.let { comments -> + if (comments.isNotEmpty()) { + data.gradeCategories.singleOrNull { + it.type == GradeCategory.TYPE_BEHAVIOUR_COMMENT + && it.categoryId == comments[0].asJsonObject.getLong("Id") + }?.text + } else null + } ?: "" + + val valueFrom = value ?: category?.valueFrom ?: 0f + val valueTo = category?.valueTo ?: 0f + + val gradeObject = Grade( + profileId, + id, + categoryName, + color, + description, + name, + valueFrom, + -1f, + semester, + teacherId, + 1 + ).apply { + type = Grade.TYPE_POINT_SUM + valueMax = valueTo + } + + data.gradeList.add(gradeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_GRADE, + id, + profile.empty, + profile.empty, + addedDate + )) + } + + data.toRemove.add(DataRemoveModel.Grades.semesterWithType(profile.currentSemester, Grade.TYPE_POINT_SUM)) + data.setSyncNext(ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADES, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiClasses.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiClasses.kt new file mode 100644 index 00000000..6f497021 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiClasses.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-14 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.DAY +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_CLASSES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.teams.Team +import pl.szczodrzynski.edziennik.getJsonObject +import pl.szczodrzynski.edziennik.getLong +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.utils.models.Date + +class LibrusApiClasses(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiClasses" + } + + init { + apiGet(TAG, "Classes") { json -> + json.getJsonObject("Class")?.also { studentClass -> + val id = studentClass.getLong("Id") ?: return@also + val name = studentClass.getString("Number") + + studentClass.getString("Symbol") + val code = data.schoolName + ":" + name + val teacherId = studentClass.getJsonObject("ClassTutor")?.getLong("Id") ?: -1 + + val teamObject = Team( + profileId, + id, + name, + 1, + code, + teacherId + ) + + data.teamList.put(id, teamObject) + + data.unitId = studentClass.getJsonObject("Unit").getLong("Id") ?: 0L + + profile?.apply { + dateSemester1Start = Date.fromY_m_d(studentClass.getString("BeginSchoolYear") + ?: return@apply) + dateSemester2Start = Date.fromY_m_d(studentClass.getString("EndFirstSemester") + ?: return@apply) + dateYearEnd = Date.fromY_m_d(studentClass.getString("EndSchoolYear") + ?: return@apply) + } + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_CLASSES, 4 * DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiClassrooms.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiClassrooms.kt new file mode 100644 index 00000000..422321ca --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiClassrooms.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-24. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_CLASSROOMS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.classrooms.Classroom +import java.util.* + +class LibrusApiClassrooms(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiClassrooms" + } + + init { + apiGet(TAG, "Classrooms") { json -> + val classrooms = json.getJsonArray("Classrooms").asJsonObjectList() + + classrooms?.forEach { classroom -> + val id = classroom.getLong("Id") ?: return@forEach + val name = classroom.getString("Name")?.toLowerCase(Locale.getDefault()) ?: "" + val symbol = classroom.getString("Symbol")?.toLowerCase(Locale.getDefault()) ?: "" + val nameShort = name.split(" ").onEach { it[0] }.joinToString() + + val friendlyName = if (name != symbol && !name.contains(symbol) && !nameShort.contains(symbol)) { + classroom.getString("Symbol") + " " + classroom.getString("Name") + } + else { + classroom.getString("Name") ?: "" + } + + data.classrooms.put(id, Classroom(profileId, id, friendlyName)) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_CLASSROOMS, 4*DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiDescriptiveGradeCategories.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiDescriptiveGradeCategories.kt new file mode 100644 index 00000000..079792c7 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiDescriptiveGradeCategories.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-29 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import android.graphics.Color +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADE_CATEGORIES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory + +class LibrusApiDescriptiveGradeCategories(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiDescriptiveGradeCategories" + } + + init { + apiGet(TAG, "DescriptiveTextGrades/Skills") { json -> + json.getJsonArray("Skills")?.asJsonObjectList()?.forEach { category -> + val id = category.getLong("Id") ?: return@forEach + val name = category.getString("Name") ?: "" + val color = category.getJsonObject("Color")?.getInt("Id") + ?.let { data.getColor(it) } ?: Color.BLUE + + val gradeCategoryObject = GradeCategory( + profileId, + id, + -1f, + color, + name + ).apply { + type = GradeCategory.TYPE_DESCRIPTIVE + } + + data.gradeCategories.put(id, gradeCategoryObject) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADE_CATEGORIES, 1 * DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiDescriptiveGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiDescriptiveGrades.kt new file mode 100644 index 00000000..bea6bd5c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiDescriptiveGrades.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-29 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_DESCRIPTIVE_TEXT +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_TEXT +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date + +class LibrusApiDescriptiveGrades(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiDescriptiveGrades" + } + + init { data.profile?.also { profile -> + apiGet(TAG, "BaseTextGrades") { json -> + + json.getJsonArray("Grades")?.asJsonObjectList()?.forEach { grade -> + val id = grade.getLong("Id") ?: return@forEach + val teacherId = grade.getJsonObject("AddedBy")?.getLong("Id") ?: return@forEach + val semester = grade.getInt("Semester") ?: return@forEach + val subjectId = grade.getJsonObject("Subject")?.getLong("Id") ?: return@forEach + val description = grade.getString("Grade") + + val categoryId = grade.getJsonObject("Skill")?.getLong("Id") + ?: grade.getJsonObject("Category")?.getLong("Id") + ?: return@forEach + val type = when (grade.getJsonObject("Category")) { + null -> TYPE_DESCRIPTIVE_TEXT + else -> TYPE_TEXT + } + + val category = data.gradeCategories.singleOrNull { + it.categoryId == categoryId && it.type == when (type) { + TYPE_DESCRIPTIVE_TEXT -> GradeCategory.TYPE_DESCRIPTIVE + else -> GradeCategory.TYPE_TEXT + } + } + + val addedDate = Date.fromIso(grade.getString("AddDate") ?: return@forEach) + + val gradeObject = Grade( + profileId, + id, + category?.text ?: "", + category?.color ?: -1, + description, + " ", + 0f, + 0f, + semester, + teacherId, + subjectId + ).apply { + this.type = type + } + + data.gradeList.add(gradeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_GRADE, + id, + profile.empty, + profile.empty, + addedDate + )) + } + + data.toRemove.addAll(listOf( + TYPE_DESCRIPTIVE_TEXT, + TYPE_TEXT + ).map { + DataRemoveModel.Grades.semesterWithType(profile.currentSemester, it) + }) + + data.setSyncNext(ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEventTypes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEventTypes.kt new file mode 100644 index 00000000..48c479c1 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEventTypes.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-24. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_EVENT_TYPES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.events.EventType + +class LibrusApiEventTypes(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiEventTypes" + } + + init { + apiGet(TAG, "HomeWorks/Categories") { json -> + val eventTypes = json.getJsonArray("Categories").asJsonObjectList() + + eventTypes?.forEach { eventType -> + val id = eventType.getLong("Id") ?: return@forEach + val name = eventType.getString("Name") ?: "" + val color = data.getColor(eventType.getJsonObject("Color")?.getInt("Id")) + + data.eventTypes.put(id, EventType(profileId, id, name, color)) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_EVENT_TYPES, 4*DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEvents.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEvents.kt new file mode 100644 index 00000000..43cddd54 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEvents.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-4. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import androidx.core.util.isEmpty +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_EVENTS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +class LibrusApiEvents(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiEvents" + } + + init { + if (data.eventTypes.isEmpty()) { + data.db.eventTypeDao().getAllNow(profileId).toSparseArray(data.eventTypes) { it.id } + } + + apiGet(TAG, "HomeWorks") { json -> + val events = json.getJsonArray("HomeWorks").asJsonObjectList() + + events?.forEach { event -> + val id = event.getLong("Id") ?: return@forEach + val eventDate = Date.fromY_m_d(event.getString("Date")) + val topic = event.getString("Content") ?: "" + val type = event.getJsonObject("Category")?.getInt("Id") ?: -1 + val teacherId = event.getJsonObject("CreatedBy")?.getLong("Id") ?: -1 + val subjectId = event.getJsonObject("Subject")?.getLong("Id") ?: -1 + val teamId = event.getJsonObject("Class")?.getLong("Id") ?: -1 + + val lessonNo = event.getInt("LessonNo") + val lessonRange = data.lessonRanges.singleOrNull { it.lessonNumber == lessonNo } + val startTime = lessonRange?.startTime ?: Time.fromH_m(event.getString("TimeFrom")) + val addedDate = Date.fromIso(event.getString("AddDate")) + + val eventObject = Event( + profileId, + id, + eventDate, + startTime, + topic, + -1, + type, + false, + teacherId, + subjectId, + teamId + ) + + data.eventList.add(eventObject) + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_EVENT, + id, + profile?.empty ?: false, + profile?.empty ?: false, + addedDate + )) + } + + data.toRemove.add(DataRemoveModel.Events.futureExceptTypes(listOf( + Event.TYPE_HOMEWORK, + Event.TYPE_PT_MEETING + ))) + + data.setSyncNext(ENDPOINT_LIBRUS_API_EVENTS, SYNC_ALWAYS) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGradeCategories.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGradeCategories.kt new file mode 100644 index 00000000..1e4b7151 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGradeCategories.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-11-5 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import android.graphics.Color +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NORMAL_GRADE_CATEGORIES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory + +class LibrusApiGradeCategories(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiGradeCategories" + } + + init { + apiGet(TAG, "Grades/Categories") { json -> + json.getJsonArray("Categories")?.asJsonObjectList()?.forEach { category -> + val id = category.getLong("Id") ?: return@forEach + val name = category.getString("Name")?.fixWhiteSpaces() ?: "" + val weight = when (category.getBoolean("CountToTheAverage")) { + true -> category.getFloat("Weight") ?: 0f + else -> 0f + } + val color = category.getJsonObject("Color")?.getInt("Id") + ?.let { data.getColor(it) } ?: Color.BLUE + + val gradeCategoryObject = GradeCategory( + profileId, + id, + weight, + color, + name + ) + + data.gradeCategories.put(id, gradeCategoryObject) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_NORMAL_GRADE_CATEGORIES, SYNC_ALWAYS) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGradeComments.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGradeComments.kt new file mode 100644 index 00000000..05031612 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGradeComments.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-11-20 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory + +class LibrusApiGradeComments(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiGradeComments" + } + + init { + apiGet(TAG, "Grades/Comments") { json -> + + json.getJsonArray("Comments")?.asJsonObjectList()?.forEach { comment -> + val id = comment.getLong("Id") ?: return@forEach + val text = comment.getString("Text")?.fixWhiteSpaces() ?: return@forEach + + val gradeCategoryObject = GradeCategory( + profileId, + id, + -1f, + -1, + text + ).apply { + type = GradeCategory.TYPE_NORMAL_COMMENT + } + + data.gradeCategories.put(id, gradeCategoryObject) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS, SYNC_ALWAYS) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGrades.kt new file mode 100644 index 00000000..17ae0d68 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGrades.kt @@ -0,0 +1,120 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NORMAL_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.* +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.models.Date + +class LibrusApiGrades(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiGrades" + } + + init { data.profile?.also { profile -> + apiGet(TAG, "Grades") { json -> + val grades = json.getJsonArray("Grades").asJsonObjectList() + + grades?.forEach { grade -> + val id = grade.getLong("Id") ?: return@forEach + val categoryId = grade.getJsonObject("Category")?.getLong("Id") ?: -1 + val name = grade.getString("Grade") ?: "" + val semester = grade.getInt("Semester") ?: return@forEach + val teacherId = grade.getJsonObject("AddedBy")?.getLong("Id") ?: -1 + val subjectId = grade.getJsonObject("Subject")?.getLong("Id") ?: -1 + val addedDate = Date.fromIso(grade.getString("AddDate")) + + val category = data.gradeCategories.singleOrNull { it.categoryId == categoryId } + val categoryName = category?.text ?: "" + val color = category?.color ?: -1 + var weight = category?.weight ?: 0f + val value = Utils.getGradeValue(name) + + + if (name == "-" || name == "+" + || name.equals("np", ignoreCase = true) + || name.equals("bz", ignoreCase = true)) { + weight = 0f + } + + val description = grade.getJsonArray("Comments")?.asJsonObjectList()?.let { comments -> + if (comments.isNotEmpty()) { + data.gradeCategories.singleOrNull { + it.type == GradeCategory.TYPE_NORMAL_COMMENT + && it.categoryId == comments[0].asJsonObject.getLong("Id") + }?.text + } else null + } ?: "" + + val gradeObject = Grade( + profileId, + id, + categoryName, + color, + description, + name, + value, + weight, + semester, + teacherId, + subjectId + ) + + when { + grade.getBoolean("IsConstituent") ?: false -> + gradeObject.type = TYPE_NORMAL + grade.getBoolean("IsSemester") ?: false -> // semester final + gradeObject.type = if (gradeObject.semester == 1) TYPE_SEMESTER1_FINAL else TYPE_SEMESTER2_FINAL + grade.getBoolean("IsSemesterProposition") ?: false -> // semester proposed + gradeObject.type = if (gradeObject.semester == 1) TYPE_SEMESTER1_PROPOSED else TYPE_SEMESTER2_PROPOSED + grade.getBoolean("IsFinal") ?: false -> // year final + gradeObject.type = TYPE_YEAR_FINAL + grade.getBoolean("IsFinalProposition") ?: false -> // year final + gradeObject.type = TYPE_YEAR_PROPOSED + } + + grade.getJsonObject("Improvement")?.also { + val historicalId = it.getLong("Id") + data.gradeList.firstOrNull { grade -> grade.id == historicalId }?.also { grade -> + grade.parentId = gradeObject.id + if (grade.name == "nb") grade.weight = 0f + } + gradeObject.isImprovement = true + } + + data.gradeList.add(gradeObject) + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_GRADE, + id, + profile.empty, + profile.empty, + addedDate + )) + } + + data.toRemove.addAll(listOf( + TYPE_NORMAL, + TYPE_SEMESTER1_FINAL, + TYPE_SEMESTER2_FINAL, + TYPE_SEMESTER1_PROPOSED, + TYPE_SEMESTER2_PROPOSED, + TYPE_YEAR_FINAL, + TYPE_YEAR_PROPOSED + ).map { + DataRemoveModel.Grades.semesterWithType(profile.currentSemester, it) + }) + data.setSyncNext(ENDPOINT_LIBRUS_API_NORMAL_GRADES, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiHomework.kt new file mode 100644 index 00000000..16bbac1c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiHomework.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-12. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_HOMEWORK +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date + +class LibrusApiHomework(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiHomework" + } + + init { + apiGet(TAG, "HomeWorkAssignments") { json -> + val homeworkList = json.getJsonArray("HomeWorkAssignments").asJsonObjectList() + + homeworkList?.forEach { homework -> + val id = homework.getLong("Id") ?: return@forEach + val eventDate = Date.fromY_m_d(homework.getString("DueDate")) + val topic = homework.getString("Topic") + "\n" + homework.getString("Text") + val teacherId = homework.getJsonObject("Teacher")?.getLong("Id") ?: -1 + val addedDate = Date.fromY_m_d(homework.getString("Date")) + + val eventObject = Event( + profileId, + id, + eventDate, + null, + topic, + -1, + -1, + false, + teacherId, + -1, + -1 + ) + + data.eventList.add(eventObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_HOMEWORK, + id, + profile?.empty ?: false, + profile?.empty ?: false, + addedDate.inMillis + )) + } + + data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_HOMEWORK)) + + data.setSyncNext(ENDPOINT_LIBRUS_API_HOMEWORK, SYNC_ALWAYS) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiLuckyNumber.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiLuckyNumber.kt new file mode 100644 index 00000000..a272a5dd --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiLuckyNumber.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-14 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.DAY +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_LUCKY_NUMBER +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.luckynumber.LuckyNumber +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.getInt +import pl.szczodrzynski.edziennik.getJsonObject +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +class LibrusApiLuckyNumber(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiLuckyNumber" + } + + init { + data.profile?.luckyNumber = -1 + data.profile?.luckyNumberDate = null + + var nextSync = System.currentTimeMillis() + 2*DAY*1000 + + apiGet(TAG, "LuckyNumbers") { json -> + if (json.isJsonNull) { + //profile?.luckyNumberEnabled = false + } else { + json.getJsonObject("LuckyNumber")?.also { luckyNumberEl -> + + val luckyNumberDate = Date.fromY_m_d(luckyNumberEl.getString("LuckyNumberDay")) ?: Date.getToday() + val luckyNumber = luckyNumberEl.getInt("LuckyNumber") ?: -1 + val luckyNumberObject = LuckyNumber( + profileId, + luckyNumberDate, + luckyNumber + ) + + //if (luckyNumberDate > Date.getToday()) { + nextSync = luckyNumberDate.combineWith(Time(15, 0, 0)) + //} + + data.luckyNumberList.add(luckyNumberObject) + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_LUCKY_NUMBER, + luckyNumberObject.date.value.toLong(), + profile?.empty ?: false, + profile?.empty ?: false, + System.currentTimeMillis() + )) + } + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_LUCKY_NUMBER, syncAt = nextSync) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiMe.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiMe.kt new file mode 100644 index 00000000..c226cf58 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiMe.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-3. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_ME +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi + +class LibrusApiMe(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiMe" + } + + init { + apiGet(TAG, "Me") { json -> + val me = json.getJsonObject("Me") + val account = me?.getJsonObject("Account") + val user = me?.getJsonObject("User") + + data.isPremium = account?.getBoolean("IsPremium") == true || account?.getBoolean("IsPremiumDemo") == true + + val isParent = account?.getInt("GroupId") == 5 + data.profile?.accountNameLong = + if (isParent) + buildFullName(account?.getString("FirstName"), account?.getString("LastName")) + else null + + data.profile?.studentNameLong = + buildFullName(user?.getString("FirstName"), user?.getString("LastName")) + + data.setSyncNext(ENDPOINT_LIBRUS_API_ME, 2*DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiNoticeTypes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiNoticeTypes.kt new file mode 100644 index 00000000..14f251b6 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiNoticeTypes.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-24. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NOTICE_TYPES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.notices.NoticeType + +class LibrusApiNoticeTypes(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiNoticeTypes" + } + + init { + apiGet(TAG, "Notes/Categories") { json -> + val noticeTypes = json.getJsonArray("Categories").asJsonObjectList() + + noticeTypes?.forEach { noticeType -> + val id = noticeType.getLong("Id") ?: return@forEach + val name = noticeType.getString("CategoryName") ?: "" + + data.noticeTypes.put(id, NoticeType(profileId, id, name)) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_NOTICE_TYPES, 4*DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiNotices.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiNotices.kt new file mode 100644 index 00000000..7d2036ca --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiNotices.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-24. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import androidx.core.util.isEmpty +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NOTICES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.notices.Notice +import pl.szczodrzynski.edziennik.utils.models.Date + +class LibrusApiNotices(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiNotices" + } + + init { + if (data.noticeTypes.isEmpty()) { + data.db.noticeTypeDao().getAllNow(profileId).toSparseArray(data.noticeTypes) { it.id } + } + + apiGet(TAG, "Notes") { json -> + val notes = json.getJsonArray("Notes").asJsonObjectList() + + notes?.forEach { note -> + val id = note.getLong("Id") ?: return@forEach + val text = note.getString("Text") ?: "" + val categoryId = note.getJsonObject("Category")?.getLong("Id") ?: -1 + val teacherId = note.getJsonObject("AddedBy")?.getLong("Id") ?: -1 + val addedDate = note.getString("Date")?.let { Date.fromY_m_d(it) } ?: return@forEach + + val type = when (note.getInt("Positive")) { + 0 -> Notice.TYPE_NEGATIVE + 1 -> Notice.TYPE_POSITIVE + /*2*/else -> Notice.TYPE_NEUTRAL + } + val categoryText = data.noticeTypes[categoryId]?.name ?: "" + val semester = profile?.dateToSemester(addedDate) ?: 1 + + val noticeObject = Notice( + profileId, + id, + categoryText+"\n"+text, + semester, + type, + teacherId + ) + + data.noticeList.add(noticeObject) + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_NOTICE, + id, + profile?.empty ?: false, + profile?.empty ?: false, + addedDate.inMillis + )) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_NOTICES, SYNC_ALWAYS) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPointGradeCategories.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPointGradeCategories.kt new file mode 100644 index 00000000..2515667b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPointGradeCategories.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-29 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import android.graphics.Color +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_POINT_GRADE_CATEGORIES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory + +class LibrusApiPointGradeCategories(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiPointGradeCategories" + } + + init { + apiGet(TAG, "PointGrades/Categories") { json -> + json.getJsonArray("Categories")?.asJsonObjectList()?.forEach { category -> + val id = category.getLong("Id") ?: return@forEach + val name = category.getString("Name") ?: "" + val color = category.getJsonObject("Color")?.getInt("Id") + ?.let { data.getColor(it) } ?: Color.BLUE + val countToAverage = category.getBoolean("CountToTheAverage") ?: true + val weight = if (countToAverage) category.getFloat("Weight") ?: 0f else 0f + val valueFrom = category.getFloat("ValueFrom") ?: 0f + val valueTo = category.getFloat("ValueTo") ?: 0f + + val gradeCategoryObject = GradeCategory( + profileId, + id, + weight, + color, + name + ).apply { + type = GradeCategory.TYPE_POINT + setValueRange(valueFrom, valueTo) + } + + data.gradeCategories.put(id, gradeCategoryObject) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_POINT_GRADE_CATEGORIES, 1 * DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPointGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPointGrades.kt new file mode 100644 index 00000000..eb9197bb --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPointGrades.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-29 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_POINT_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_POINT_AVG +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date + +class LibrusApiPointGrades(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiPointGrades" + } + + init { data.profile?.also { profile -> + apiGet(TAG, "PointGrades") { json -> + + json.getJsonArray("Grades")?.asJsonObjectList()?.forEach { grade -> + val id = grade.getLong("Id") ?: return@forEach + val teacherId = grade.getJsonObject("AddedBy")?.getLong("Id") ?: return@forEach + val semester = grade.getInt("Semester") ?: return@forEach + val subjectId = grade.getJsonObject("Subject")?.getLong("Id") ?: return@forEach + val name = grade.getString("Grade") ?: return@forEach + val value = grade.getFloat("GradeValue") ?: 0f + + val categoryId = grade.getJsonObject("Category")?.getLong("Id") ?: return@forEach + + val category = data.gradeCategories.singleOrNull { + it.categoryId == categoryId && it.type == GradeCategory.TYPE_POINT + } + + val addedDate = Date.fromIso(grade.getString("AddDate") ?: return@forEach) + + val gradeObject = Grade( + profileId, + id, + category?.text ?: "", + category?.color ?: -1, + "", + name, + value, + category?.weight ?: 0f, + semester, + teacherId, + subjectId + ).apply { + type = TYPE_POINT_AVG + valueMax = category?.valueTo ?: 0f + } + + data.gradeList.add(gradeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_GRADE, + id, + profile.empty, + profile.empty, + addedDate + )) + } + + data.toRemove.add(DataRemoveModel.Grades.semesterWithType(profile.currentSemester, TYPE_POINT_AVG)) + + data.setSyncNext(ENDPOINT_LIBRUS_API_POINT_GRADES, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPtMeetings.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPtMeetings.kt new file mode 100644 index 00000000..ab78a6e8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPtMeetings.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-24. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_PT_MEETINGS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +class LibrusApiPtMeetings(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiPtMeetings" + } + + init { + apiGet(TAG, "ParentTeacherConferences") { json -> + val ptMeetings = json.getJsonArray("ParentTeacherConferences").asJsonObjectList() + + ptMeetings?.forEach { meeting -> + val id = meeting.getLong("Id") ?: return@forEach + val topic = meeting.getString("Topic") ?: "" + val teacherId = meeting.getJsonObject("Teacher")?.getLong("Id") ?: -1 + val eventDate = meeting.getString("Date")?.let { Date.fromY_m_d(it) } ?: return@forEach + val startTime = meeting.getString("Time")?.let { + if (it == "00:00:00") + null + else + Time.fromH_m_s(it) + } + + val eventObject = Event( + profileId, + id, + eventDate, + startTime, + topic, + -1, + Event.TYPE_PT_MEETING, + false, + teacherId, + -1, + data.teamClass?.id ?: -1 + ) + + data.eventList.add(eventObject) + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_EVENT, + id, + profile?.empty ?: false, + profile?.empty ?: false, + System.currentTimeMillis() + )) + } + + data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_PT_MEETING)) + + data.setSyncNext(ENDPOINT_LIBRUS_API_PT_MEETINGS, 12*HOUR) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiSchools.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiSchools.kt new file mode 100644 index 00000000..c47347e0 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiSchools.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-4. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_SCHOOLS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonRange +import pl.szczodrzynski.edziennik.utils.models.Time +import java.util.* + +class LibrusApiSchools(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiSchools" + } + + init { + apiGet(TAG, "Schools") { json -> + val school = json.getJsonObject("School") + val schoolId = school?.getInt("Id") + val schoolNameLong = school?.getString("Name") + + // create the school's short name using first letters of each long name's word + // append the town name and save to student data + val schoolNameShort = schoolNameLong?.firstLettersName + val schoolTown = school?.getString("Town")?.toLowerCase(Locale.getDefault()) + data.schoolName = schoolId.toString() + schoolNameShort + "_" + schoolTown + + school?.getJsonArray("LessonsRange")?.let { ranges -> + data.lessonRanges.clear() + ranges.forEachIndexed { index, rangeEl -> + val range = rangeEl.asJsonObject + val from = range.getString("From") ?: return@forEachIndexed + val to = range.getString("To") ?: return@forEachIndexed + data.lessonRanges.put( + index, + LessonRange( + profileId, + index, + Time.fromH_m(from), + Time.fromH_m(to) + )) + } + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_SCHOOLS, 4 * DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiSubjects.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiSubjects.kt new file mode 100644 index 00000000..4c383933 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiSubjects.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-23. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_SUBJECTS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject + +class LibrusApiSubjects(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiSubjects" + } + + init { + apiGet(TAG, "Subjects") { json -> + val subjects = json.getJsonArray("Subjects").asJsonObjectList() + + subjects?.forEach { subject -> + val id = subject.getLong("Id") ?: return@forEach + val longName = subject.getString("Name") ?: "" + val shortName = subject.getString("Short") ?: "" + + data.subjectList.put(id, Subject(profileId, id, longName, shortName)) + } + + data.subjectList.put(1, Subject(profileId, 1, "Zachowanie", "zach")) + + data.setSyncNext(ENDPOINT_LIBRUS_API_SUBJECTS, 4*DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDayTypes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDayTypes.kt new file mode 100644 index 00000000..88c8ce14 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDayTypes.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-19 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_TEACHER_FREE_DAY_TYPES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.teachers.TeacherAbsenceType + +class LibrusApiTeacherFreeDayTypes(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiTeacherFreeDayTypes" + } + + init { + apiGet(TAG, "TeacherFreeDays/Types") { json -> + val teacherAbsenceTypes = json.getJsonArray("Types").asJsonObjectList() + + teacherAbsenceTypes?.forEach { teacherAbsenceType -> + val id = teacherAbsenceType.getLong("Id") ?: return@forEach + val name = teacherAbsenceType.getString("Name") ?: return@forEach + + val teacherAbsenceTypeObject = TeacherAbsenceType( + profileId, + id, + name + ) + + data.teacherAbsenceTypes.put(id, teacherAbsenceTypeObject) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_TEACHER_FREE_DAY_TYPES, 7 * DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDays.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDays.kt new file mode 100644 index 00000000..1069ffa3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDays.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-4. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import androidx.core.util.isEmpty +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_AGENDA +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.teachers.TeacherAbsence +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +class LibrusApiTeacherFreeDays(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiTeacherFreeDays" + } + + init { + if (data.teacherAbsenceTypes.isEmpty()) { + data.db.teacherAbsenceTypeDao().getAllNow(profileId).toSparseArray(data.teacherAbsenceTypes) { it.id } + } + + apiGet(TAG, "TeacherFreeDays") { json -> + val teacherAbsences = json.getJsonArray("TeacherFreeDays").asJsonObjectList() + + teacherAbsences?.forEach { teacherAbsence -> + val id = teacherAbsence.getLong("Id") ?: return@forEach + val teacherId = teacherAbsence.getJsonObject("Teacher")?.getLong("Id") + ?: return@forEach + val type = teacherAbsence.getJsonObject("Type").getLong("Id") ?: return@forEach + val name = data.teacherAbsenceTypes.singleOrNull { it.id == type }?.name + val dateFrom = Date.fromY_m_d(teacherAbsence.getString("DateFrom")) + val dateTo = Date.fromY_m_d(teacherAbsence.getString("DateTo")) + val timeFrom = teacherAbsence.getString("TimeFrom")?.let { Time.fromH_m_s(it) } + val timeTo = teacherAbsence.getString("TimeTo")?.let { Time.fromH_m_s(it) } + + val teacherAbsenceObject = TeacherAbsence( + profileId, + id, + teacherId, + type, + name, + dateFrom, + dateTo, + timeFrom, + timeTo + ) + + data.teacherAbsenceList.add(teacherAbsenceObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_TEACHER_ABSENCE, + id, + profile?.empty ?: false, + profile?.empty ?: false, + System.currentTimeMillis() + )) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS, 6*HOUR, DRAWER_ITEM_AGENDA) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTemplate.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTemplate.kt new file mode 100644 index 00000000..3b111f0c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTemplate.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-4. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi + +class LibrusApiTemplate(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApi" + } + + init { + /*apiGet(TAG, "") { json -> + + data.setSyncNext(ENDPOINT_LIBRUS_API_, SYNC_ALWAYS) + onSuccess() + }*/ + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTextGradeCategories.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTextGradeCategories.kt new file mode 100644 index 00000000..5ee96929 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTextGradeCategories.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-29 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import android.graphics.Color +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_TEXT_GRADE_CATEGORIES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory + +class LibrusApiTextGradeCategories(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiTextGradeCategories" + } + + init { + apiGet(TAG, "TextGrades/Categories") { json -> + json.getJsonArray("Categories")?.asJsonObjectList()?.forEach { category -> + val id = category.getLong("Id") ?: return@forEach + val name = category.getString("Name") ?: "" + val color = category.getJsonObject("Color")?.getInt("Id") + ?.let { data.getColor(it) } ?: Color.BLUE + + val gradeCategoryObject = GradeCategory( + profileId, + id, + -1f, + color, + name + ).apply { + type = GradeCategory.TYPE_TEXT + } + + data.gradeCategories.put(id, gradeCategoryObject) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_TEXT_GRADE_CATEGORIES, 1 * DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTextGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTextGrades.kt new file mode 100644 index 00000000..0281722f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTextGrades.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-29 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_TEXT_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.TYPE_DESCRIPTIVE +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date + +class LibrusApiTextGrades(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiTextGrades" + } + + init { data.profile?.also { profile -> + apiGet(TAG, "DescriptiveGrades") { json -> + + json.getJsonArray("Grades")?.asJsonObjectList()?.forEach { grade -> + val id = grade.getLong("Id") ?: return@forEach + val teacherId = grade.getJsonObject("AddedBy")?.getLong("Id") ?: return@forEach + val semester = grade.getInt("Semester") ?: return@forEach + val subjectId = grade.getJsonObject("Subject")?.getLong("Id") ?: return@forEach + val description = grade.getString("RealGradeValue") ?: grade.getString("Map") ?: "" + + val categoryId = grade.getJsonObject("Skill")?.getLong("Id") ?: return@forEach + + val category = data.gradeCategories.singleOrNull { + it.categoryId == categoryId && it.type == GradeCategory.TYPE_DESCRIPTIVE + } + + val addedDate = Date.fromIso(grade.getString("AddDate") ?: return@forEach) + + val gradeObject = Grade( + profileId, + id, + category?.text ?: "", + category?.color ?: -1, + "", + description, + 0f, + 0f, + semester, + teacherId, + subjectId + ).apply { + type = TYPE_DESCRIPTIVE + } + + data.gradeList.add(gradeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_GRADE, + id, + profile.empty, + profile.empty, + addedDate + )) + } + + data.toRemove.add(DataRemoveModel.Grades.semesterWithType(profile.currentSemester, TYPE_DESCRIPTIVE)) + + data.setSyncNext(ENDPOINT_LIBRUS_API_TEXT_GRADES, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTimetables.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTimetables.kt new file mode 100644 index 00000000..a43d6e34 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTimetables.kt @@ -0,0 +1,205 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-10. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import androidx.core.util.isEmpty +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_TIMETABLES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson +import pl.szczodrzynski.edziennik.utils.Utils.d +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.edziennik.utils.models.Week + +class LibrusApiTimetables(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiTimetables" + } + + init { + if (data.classrooms.isEmpty()) { + data.db.classroomDao().getAllNow(profileId).toSparseArray(data.classrooms) { it.id } + } + + val currentWeekStart = Week.getWeekStart() + + if (Date.getToday().weekDay > 4) { + currentWeekStart.stepForward(0, 0, 7) + } + + val getDate = data.arguments?.getString("weekStart") ?: currentWeekStart.stringY_m_d + + val weekStart = Date.fromY_m_d(getDate) + val weekEnd = weekStart.clone().stepForward(0, 0, 6) + + apiGet(TAG, "Timetables?weekStart=${weekStart.stringY_m_d}") { json -> + val days = json.getJsonObject("Timetable") + + days?.entrySet()?.forEach { (dateString, dayEl) -> + val day = dayEl?.asJsonArray + + val lessonDate = dateString?.let { Date.fromY_m_d(it) } ?: return@forEach + + var lessonsFound = false + day?.forEach { lessonRangeEl -> + val lessonRange = lessonRangeEl?.asJsonArray?.asJsonObjectList() + if (lessonRange?.isNullOrEmpty() == false) + lessonsFound = true + lessonRange?.forEach { lesson -> + parseLesson(lessonDate, lesson) + } + } + + if (day.isNullOrEmpty() || !lessonsFound) { + data.lessonNewList.add(Lesson(profileId, lessonDate.value.toLong()).apply { + type = Lesson.TYPE_NO_LESSONS + date = lessonDate + }) + } + } + + d(TAG, "Clearing lessons between ${weekStart.stringY_m_d} and ${weekEnd.stringY_m_d} - timetable downloaded for $getDate") + + if (data.timetableNotPublic) data.timetableNotPublic = false + + data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd)) + data.setSyncNext(ENDPOINT_LIBRUS_API_TIMETABLES, SYNC_ALWAYS) + onSuccess() + } + } + + private fun parseLesson(lessonDate: Date, lesson: JsonObject) { data.profile?.also { profile -> + val isSubstitution = lesson.getBoolean("IsSubstitutionClass") ?: false + val isCancelled = lesson.getBoolean("IsCanceled") ?: false + + val lessonNo = lesson.getInt("LessonNo") ?: return + val startTime = lesson.getString("HourFrom")?.let { Time.fromH_m(it) } ?: return + val endTime = lesson.getString("HourTo")?.let { Time.fromH_m(it) } ?: return + val subjectId = lesson.getJsonObject("Subject")?.getLong("Id") + val teacherId = lesson.getJsonObject("Teacher")?.getLong("Id") + val classroomId = lesson.getJsonObject("Classroom")?.getLong("Id") ?: -1 + val virtualClassId = lesson.getJsonObject("VirtualClass")?.getLong("Id") + val teamId = lesson.getJsonObject("Class")?.getLong("Id") ?: virtualClassId + + val lessonObject = Lesson(profileId, -1) + + if (isSubstitution && isCancelled) { + // shifted lesson - source + val newDate = lesson.getString("NewDate")?.let { Date.fromY_m_d(it) } ?: return + val newLessonNo = lesson.getInt("NewLessonNo") ?: return + val newStartTime = lesson.getString("NewHourFrom")?.let { Time.fromH_m(it) } ?: return + val newEndTime = lesson.getString("NewHourTo")?.let { Time.fromH_m(it) } ?: return + val newSubjectId = lesson.getJsonObject("NewSubject")?.getLong("Id") + val newTeacherId = lesson.getJsonObject("NewTeacher")?.getLong("Id") + val newClassroomId = lesson.getJsonObject("NewClassroom")?.getLong("Id") ?: -1 + val newVirtualClassId = lesson.getJsonObject("NewVirtualClass")?.getLong("Id") + val newTeamId = lesson.getJsonObject("NewClass")?.getLong("Id") ?: newVirtualClassId + + lessonObject.let { + it.type = Lesson.TYPE_SHIFTED_SOURCE + it.oldDate = lessonDate + it.oldLessonNumber = lessonNo + it.oldStartTime = startTime + it.oldEndTime = endTime + it.oldSubjectId = subjectId + it.oldTeacherId = teacherId + it.oldTeamId = teamId + it.oldClassroom = data.classrooms[classroomId]?.name + + it.date = newDate + it.lessonNumber = newLessonNo + it.startTime = newStartTime + it.endTime = newEndTime + it.subjectId = newSubjectId + it.teacherId = newTeacherId + it.teamId = newTeamId + it.classroom = data.classrooms[newClassroomId]?.name + } + } + else if (isSubstitution) { + // lesson change OR shifted lesson - target + val oldDate = lesson.getString("OrgDate")?.let { Date.fromY_m_d(it) } ?: return + val oldLessonNo = lesson.getInt("OrgLessonNo") ?: return + val oldStartTime = lesson.getString("OrgHourFrom")?.let { Time.fromH_m(it) } ?: return + val oldEndTime = lesson.getString("OrgHourTo")?.let { Time.fromH_m(it) } ?: return + val oldSubjectId = lesson.getJsonObject("OrgSubject")?.getLong("Id") + val oldTeacherId = lesson.getJsonObject("OrgTeacher")?.getLong("Id") + val oldClassroomId = lesson.getJsonObject("OrgClassroom")?.getLong("Id") ?: -1 + val oldVirtualClassId = lesson.getJsonObject("OrgVirtualClass")?.getLong("Id") + val oldTeamId = lesson.getJsonObject("OrgClass")?.getLong("Id") ?: oldVirtualClassId + + lessonObject.let { + it.type = if (lessonDate == oldDate && lessonNo == oldLessonNo) Lesson.TYPE_CHANGE else Lesson.TYPE_SHIFTED_TARGET + it.oldDate = oldDate + it.oldLessonNumber = oldLessonNo + it.oldStartTime = oldStartTime + it.oldEndTime = oldEndTime + it.oldSubjectId = oldSubjectId + it.oldTeacherId = oldTeacherId + it.oldTeamId = oldTeamId + it.oldClassroom = data.classrooms[oldClassroomId]?.name + + it.date = lessonDate + it.lessonNumber = lessonNo + it.startTime = startTime + it.endTime = endTime + it.subjectId = subjectId + it.teacherId = teacherId + it.teamId = teamId + it.classroom = data.classrooms[classroomId]?.name + } + } + else if (isCancelled) { + lessonObject.let { + it.type = Lesson.TYPE_CANCELLED + it.oldDate = lessonDate + it.oldLessonNumber = lessonNo + it.oldStartTime = startTime + it.oldEndTime = endTime + it.oldSubjectId = subjectId + it.oldTeacherId = teacherId + it.oldTeamId = teamId + it.oldClassroom = data.classrooms[classroomId]?.name + } + } + else { + lessonObject.let { + it.type = Lesson.TYPE_NORMAL + it.date = lessonDate + it.lessonNumber = lessonNo + it.startTime = startTime + it.endTime = endTime + it.subjectId = subjectId + it.teacherId = teacherId + it.teamId = teamId + it.classroom = data.classrooms[classroomId]?.name + } + } + + lessonObject.id = lessonObject.buildId() + + val seen = profile.empty || lessonDate < Date.getToday() + + if (lessonObject.type != Lesson.TYPE_NORMAL) { + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_LESSON_CHANGE, + lessonObject.id, + seen, + seen, + System.currentTimeMillis() + )) + } + data.lessonNewList.add(lessonObject) + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiUnits.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiUnits.kt new file mode 100644 index 00000000..be4b0087 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiUnits.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-23. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_UNITS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi + +class LibrusApiUnits(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiUnits" + } + + init { run { + if (data.unitId == 0L) { + data.setSyncNext(ENDPOINT_LIBRUS_API_UNITS, 12 * DAY) + onSuccess() + return@run + } + + apiGet(TAG, "Units") { json -> + val units = json.getJsonArray("Units").asJsonObjectList() + + units?.singleOrNull { it.getLong("Id") == data.unitId }?.also { unit -> + val startPoints = unit.getJsonObject("BehaviourGradesSettings")?.getJsonObject("StartPoints") + startPoints?.apply { + data.startPointsSemester1 = getInt("Semester1", defaultValue = 0) + data.startPointsSemester2 = getInt("Semester2", defaultValue = data.startPointsSemester1) + } + unit.getJsonObject("GradesSettings")?.apply { + data.enablePointGrades = getBoolean("PointGradesEnabled", true) + data.enableDescriptiveGrades = getBoolean("DescriptiveGradesEnabled", true) + } + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_UNITS, 7 * DAY) + onSuccess() + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiUsers.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiUsers.kt new file mode 100644 index 00000000..fb95c9ac --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiUsers.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-23. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_USERS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher + +class LibrusApiUsers(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiUsers" + } + + init { + apiGet(TAG, "Users") { json -> + val users = json.getJsonArray("Users").asJsonObjectList() + + users?.forEach { user -> + val id = user.getLong("Id") ?: return@forEach + val firstName = user.getString("FirstName")?.fixName() ?: "" + val lastName = user.getString("LastName")?.fixName() ?: "" + + val teacher = Teacher(profileId, id, firstName, lastName) + + if (user.getBoolean("IsSchoolAdministrator") == true) + teacher.setTeacherType(Teacher.TYPE_SCHOOL_ADMIN) + if (user.getBoolean("IsPedagogue") == true) + teacher.setTeacherType(Teacher.TYPE_PEDAGOGUE) + + data.teacherList.put(id, teacher) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_USERS, 4*DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiVirtualClasses.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiVirtualClasses.kt new file mode 100644 index 00000000..e8249978 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiVirtualClasses.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-23. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_VIRTUAL_CLASSES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.db.modules.teams.Team + +class LibrusApiVirtualClasses(override val data: DataLibrus, + val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + const val TAG = "LibrusApiVirtualClasses" + } + + init { + apiGet(TAG, "VirtualClasses") { json -> + val virtualClasses = json.getJsonArray("VirtualClasses").asJsonObjectList() + + virtualClasses?.forEach { virtualClass -> + val id = virtualClass.getLong("Id") ?: return@forEach + val name = virtualClass.getString("Name") ?: "" + val teacherId = virtualClass.getJsonObject("Teacher")?.getLong("Id") ?: -1 + val code = "${data.schoolName}:$name" + + data.teamList.put(id, Team(profileId, id, name, 2, code, teacherId)) + } + + data.setSyncNext(ENDPOINT_LIBRUS_API_VIRTUAL_CLASSES, 4*DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetAttachment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetAttachment.kt new file mode 100644 index 00000000..ed447ea8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetAttachment.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-11-24 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages + +import kotlinx.coroutines.* +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.ERROR_FILE_DOWNLOAD +import pl.szczodrzynski.edziennik.data.api.EXCEPTION_LIBRUS_MESSAGES_REQUEST +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages +import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent +import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent.Companion.TYPE_FINISHED +import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent.Companion.TYPE_PROGRESS +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.utils.Utils +import java.io.File +import kotlin.coroutines.CoroutineContext + +class LibrusMessagesGetAttachment( + override val data: DataLibrus, val message: Message, val attachmentId: Long, + val attachmentName: String, val onSuccess: () -> Unit) : LibrusMessages(data), CoroutineScope { + companion object { + const val TAG = "LibrusMessagesGetAttachment" + } + + private var job = Job() + + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Default + + private var getAttachmentCheckKeyTries = 0 + + init { + messagesGet(TAG, "GetFileDownloadLink", parameters = mapOf( + "fileId" to attachmentId, + "msgId" to message.id, + "archive" to 0 + )) { doc -> + val downloadLink = doc.select("response GetFileDownloadLink downloadLink").text() + val keyMatcher = Regexes.LIBRUS_ATTACHMENT_KEY.find(downloadLink) + + if (keyMatcher != null) { + getAttachmentCheckKeyTries = 0 + + val attachmentKey = keyMatcher[1] + getAttachmentCheckKey(attachmentKey) { + downloadAttachment(attachmentKey) + } + } else { + data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD) + .withApiResponse(doc.toString())) + } + } + } + + private fun getAttachmentCheckKey(attachmentKey: String, callback: () -> Unit) { + sandboxGet(TAG, "CSCheckKey", + parameters = mapOf("singleUseKey" to attachmentKey)) { json -> + + when (json.getString("status")) { + "not_downloaded_yet" -> { + if (getAttachmentCheckKeyTries++ > 5) { + data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD) + .withApiResponse(json)) + return@sandboxGet + } + launch { + delay(2000) + getAttachmentCheckKey(attachmentKey, callback) + } + } + + "ready" -> { + launch { callback() } + } + + else -> { + data.error(ApiError(TAG, EXCEPTION_LIBRUS_MESSAGES_REQUEST) + .withApiResponse(json)) + } + } + } + } + + private fun downloadAttachment(attachmentKey: String) { + val targetFile = File(Utils.getStorageDir(), attachmentName) + + sandboxGetFile(TAG, "CSDownload&singleUseKey=$attachmentKey", targetFile, { file -> + + val event = AttachmentGetEvent( + profileId, + message.id, + attachmentId, + TYPE_FINISHED, + file.absolutePath + ) + + val attachmentDataFile = File(Utils.getStorageDir(), ".${profileId}_${event.messageId}_${event.attachmentId}") + Utils.writeStringToFile(attachmentDataFile, event.fileName) + + EventBus.getDefault().post(event) + + onSuccess() + + }) { written, _ -> + val event = AttachmentGetEvent( + profileId, + message.id, + attachmentId, + TYPE_PROGRESS, + bytesWritten = written + ) + + EventBus.getDefault().post(event) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetList.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetList.kt new file mode 100644 index 00000000..e05e6232 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetList.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-24 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages + +import pl.szczodrzynski.edziennik.DAY +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES +import pl.szczodrzynski.edziennik.data.api.ERROR_NOT_IMPLEMENTED +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_MESSAGES_RECEIVED +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_MESSAGES_SENT +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.fixName +import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.models.Date + +class LibrusMessagesGetList(override val data: DataLibrus, private val type: Int = TYPE_RECEIVED, + archived: Boolean = false, val onSuccess: () -> Unit) : LibrusMessages(data) { + companion object { + const val TAG = "LibrusMessagesGetList" + } + + init { + val endpoint = when (type) { + TYPE_RECEIVED -> "Inbox/action/GetList" + Message.TYPE_SENT -> "Outbox/action/GetList" + else -> null + } + + if (endpoint != null) { + messagesGet(TAG, endpoint, parameters = mapOf( + "archive" to if (archived) 1 else 0 + )) { doc -> + doc.select("GetList data").firstOrNull()?.children()?.forEach { element -> + val id = element.select("messageId").text().toLong() + val subject = element.select("topic").text().trim() + val readDateText = element.select("readDate").text().trim() + val readDate = when (readDateText.isNotBlank()) { + true -> Date.fromIso(readDateText) + else -> 0 + } + val sentDate = Date.fromIso(element.select("sendDate").text().trim()) + + val recipientFirstName = element.select(when (type) { + TYPE_RECEIVED -> "senderFirstName" + else -> "receiverFirstName" + }).text().fixName() + + val recipientLastName = element.select(when (type) { + TYPE_RECEIVED -> "senderLastName" + else -> "receiverLastName" + }).text().fixName() + + val recipientId = data.teacherList.singleOrNull { + it.name == recipientFirstName && it.surname == recipientLastName + }?.id ?: { + val teacherObject = Teacher( + profileId, + -1 * Utils.crc16("$recipientFirstName $recipientLastName".toByteArray()).toLong(), + recipientFirstName, + recipientLastName + ) + data.teacherList.put(teacherObject.id, teacherObject) + teacherObject.id + }.invoke() + + val senderId = when (type) { + TYPE_RECEIVED -> recipientId + else -> -1 + } + + val receiverId = when (type) { + TYPE_RECEIVED -> -1 + else -> recipientId + } + + val notified = when (type) { + Message.TYPE_SENT -> true + else -> readDate > 0 || profile?.empty ?: false + } + + val messageObject = Message( + profileId, + id, + subject, + null, + type, + senderId, + -1 + ) + + val messageRecipientObject = MessageRecipient( + profileId, + receiverId, + -1, + readDate, + id + ) + + data.messageIgnoreList.add(messageObject) + data.messageRecipientList.add(messageRecipientObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_MESSAGE, + id, + notified, + notified, + sentDate + )) + } + + when (type) { + TYPE_RECEIVED -> data.setSyncNext(ENDPOINT_LIBRUS_MESSAGES_RECEIVED, SYNC_ALWAYS) + Message.TYPE_SENT -> data.setSyncNext(ENDPOINT_LIBRUS_MESSAGES_SENT, DAY, DRAWER_ITEM_MESSAGES) + } + onSuccess() + } + } else { + data.error(TAG, ERROR_NOT_IMPLEMENTED) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetMessage.kt new file mode 100644 index 00000000..5bdfc42d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetMessage.kt @@ -0,0 +1,169 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-11-11 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages + +import android.util.Base64 +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages +import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipientFull +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.fixName +import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.notEmptyOrNull +import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.utils.models.Date +import java.nio.charset.Charset + +class LibrusMessagesGetMessage( + override val data: DataLibrus, + private val messageObject: MessageFull, + val onSuccess: () -> Unit +) : LibrusMessages(data) { + companion object { + const val TAG = "LibrusMessagesGetMessage" + } + + init { data.profile?.also { profile -> + messagesGet(TAG, "GetMessage", parameters = mapOf( + "messageId" to messageObject.id, + "archive" to 0 + )) { doc -> + val message = doc.select("response GetMessage data").first() + + val body = Base64.decode(message.select("Message").text(), Base64.DEFAULT) + .toString(Charset.defaultCharset()) + .replace("\n", "
    ") + .replace("", "") + + messageObject.apply { + this.body = body + + clearAttachments() + message.select("attachments ArrayItem").forEach { + val attachmentId = it.select("id").text().toLong() + val attachmentName = it.select("filename").text() + addAttachment(attachmentId, attachmentName, -1) + } + } + + val messageRecipientList = mutableListOf() + + when (messageObject.type) { + TYPE_RECEIVED -> { + val senderLoginId = message.select("senderId").text().notEmptyOrNull() + val senderGroupId = message.select("senderGroupId").text().toIntOrNull() + val userClass = message.select("userClass").text().notEmptyOrNull() + data.teacherList.singleOrNull { it.id == messageObject.senderId }?.apply { + loginId = senderLoginId + setTeacherType(when (senderGroupId) { + /* https://api.librus.pl/2.0/Messages/Role */ + 0, 1, 99 -> Teacher.TYPE_SUPER_ADMIN + 2 -> Teacher.TYPE_SCHOOL_ADMIN + 3 -> Teacher.TYPE_PRINCIPAL + 4 -> Teacher.TYPE_TEACHER + 5, 9 -> { + if (typeDescription == null) + typeDescription = userClass + Teacher.TYPE_PARENT + } + 7 -> Teacher.TYPE_SECRETARIAT + 8 -> { + if (typeDescription == null) + typeDescription = userClass + Teacher.TYPE_STUDENT + } + 10 -> Teacher.TYPE_PEDAGOGUE + 11 -> Teacher.TYPE_LIBRARIAN + 12 -> Teacher.TYPE_SPECIALIST + 21 -> { + typeDescription = "Jednostka Nadrzędna" + Teacher.TYPE_OTHER + } + 50 -> { + typeDescription = "Jednostka Samorządu Terytorialnego" + Teacher.TYPE_OTHER + } + else -> Teacher.TYPE_OTHER + }) + } + + val readDateText = message.select("readDate").text() + val readDate = when (readDateText.isNotNullNorEmpty()) { + true -> Date.fromIso(readDateText) + else -> 0 + } + + val messageRecipientObject = MessageRecipientFull( + profileId, + -1, + -1, + readDate, + messageObject.id + ) + + messageRecipientObject.fullName = profile.accountNameLong ?: profile.studentNameLong ?: "" + + messageRecipientList.add(messageRecipientObject) + } + + TYPE_SENT -> { + + message.select("receivers ArrayItem").forEach { receiver -> + val receiverFirstName = receiver.select("firstName").text().fixName() + val receiverLastName = receiver.select("lastName").text().fixName() + val receiverLoginId = receiver.select("receiverId").text() + + val teacher = data.teacherList.singleOrNull { it.name == receiverFirstName && it.surname == receiverLastName } + val receiverId = teacher?.id ?: -1 + teacher?.loginId = receiverLoginId + + val readDateText = message.select("readed").text() + val readDate = when (readDateText.isNotNullNorEmpty()) { + true -> Date.fromIso(readDateText) + else -> 0 + } + + val messageRecipientObject = MessageRecipientFull( + profileId, + receiverId, + -1, + readDate, + messageObject.id + ) + + messageRecipientObject.fullName = "$receiverFirstName $receiverLastName" + + messageRecipientList.add(messageRecipientObject) + } + } + } + + if (!messageObject.seen) { + data.setSeenMetadataList.add(Metadata( + messageObject.profileId, + Metadata.TYPE_MESSAGE, + messageObject.id, + true, + true, + messageObject.addedDate + )) + } + + messageObject.recipients = messageRecipientList + data.messageRecipientList.addAll(messageRecipientList) + data.messageList.add(messageObject) + + EventBus.getDefault().postSticky(MessageGetEvent(messageObject)) + onSuccess() + } + } ?: onSuccess()} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetRecipientList.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetRecipientList.kt new file mode 100644 index 00000000..e8d88a5f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetRecipientList.kt @@ -0,0 +1,174 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-31. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages + +import androidx.core.util.set +import androidx.room.OnConflictStrategy +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages +import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher + +class LibrusMessagesGetRecipientList( + override val data: DataLibrus, val onSuccess: () -> Unit) : LibrusMessages(data) { + companion object { + private const val TAG = "LibrusMessagesGetRecipientList" + } + + private val listTypes = mutableListOf>() + + init { + messagesGet(TAG, "Receivers/action/GetTypes", parameters = mapOf( + "includeClass" to 1 + )) { doc -> + doc.select("response GetTypes data list ArrayItem")?.forEach { + val id = it.getElementsByTag("id")?.firstOrNull()?.ownText() ?: return@forEach + val name = it.getElementsByTag("name")?.firstOrNull()?.ownText() ?: return@forEach + listTypes += id to name + } + + getLists() + } + } + + private fun getLists() { + if (listTypes.isEmpty()) { + finish() + return + } + val type = listTypes.removeAt(0) + if (type.first == "contactsGroups") { + getLists() + return + } + messagesGetJson(TAG, "Receivers/action/GetListForType", parameters = mapOf( + "receiverType" to type.first + )) { json -> + val dataEl = json?.getJsonObject("response")?.getJsonObject("GetListForType")?.get("data") + if (dataEl is JsonObject) { + val listEl = dataEl.get("ArrayItem") + if (listEl is JsonArray) { + listEl.asJsonObjectList()?.forEach { item -> + processElement(item, type.first, type.second) + } + } + if (listEl is JsonObject) { + processElement(listEl, type.first, type.second) + } + } + + getLists() + } + } + + private fun processElement(element: JsonObject, typeId: String, typeName: String, listName: String? = null) { + val listEl = element.getJsonObject("list")?.get("ArrayItem") + if (listEl is JsonArray) { + listEl.asJsonObjectList()?.let { list -> + val label = element.getString("label") ?: "" + list.forEach { item -> + processElement(item, typeId, typeName, label) + } + return + } + } + if (listEl is JsonObject) { + val label = element.getString("label") ?: "" + processElement(listEl, typeId, typeName, label) + return + } + processRecipient(element, typeId, typeName, listName) + } + + private fun processRecipient(recipient: JsonObject, typeId: String, typeName: String, listName: String? = null) { + val id = recipient.getLong("id") ?: return + val label = recipient.getString("label") ?: return + + val fullNameLastFirst: String + val description: String? + if (typeId == "parentsCouncil" || typeId == "schoolParentsCouncil") { + val delimiterIndex = label.lastIndexOf(" - ") + if (delimiterIndex == -1) { + fullNameLastFirst = label.fixName() + description = null + } + else { + fullNameLastFirst = label.substring(0, delimiterIndex).fixName() + description = label.substring(delimiterIndex+3) + } + } + else { + fullNameLastFirst = label.fixName() + description = null + } + + var typeDescription: String? = null + val type = when (typeId) { + "tutors" -> Teacher.TYPE_EDUCATOR + "teachers" -> Teacher.TYPE_TEACHER + "classParents" -> Teacher.TYPE_PARENT + "guardians" -> Teacher.TYPE_PARENT + "parentsCouncil" -> { + typeDescription = joinNotNullStrings(": ", listName, description) + Teacher.TYPE_PARENTS_COUNCIL + } + "schoolParentsCouncil" -> { + typeDescription = joinNotNullStrings(": ", listName, description) + Teacher.TYPE_SCHOOL_PARENTS_COUNCIL + } + "pedagogue" -> Teacher.TYPE_PEDAGOGUE + "librarian" -> Teacher.TYPE_LIBRARIAN + "admin" -> Teacher.TYPE_SCHOOL_ADMIN + "secretary" -> Teacher.TYPE_SECRETARIAT + "sadmin" -> Teacher.TYPE_SUPER_ADMIN + else -> { + typeDescription = typeName + Teacher.TYPE_OTHER + } + } + + // get teacher by fullName AND type or create it + val teacher = data.teacherList.singleOrNull { + it.fullNameLastFirst == fullNameLastFirst && ((type != Teacher.TYPE_SCHOOL_ADMIN && type != Teacher.TYPE_PEDAGOGUE) || it.isType(type)) + } ?: Teacher(data.profileId, id).apply { + if (typeId == "sadmin" && id == 2L) { + name = "Pomoc" + surname = "Techniczna LIBRUS" + } + else { + name = fullNameLastFirst + fullNameLastFirst.splitName()?.let { + name = it.second + surname = it.first + } + } + data.teacherList[id] = this + } + + teacher.apply { + this.loginId = id.toString() + this.setTeacherType(type) + if (this.typeDescription.isNullOrBlank()) + this.typeDescription = typeDescription + } + } + + private fun finish() { + val event = RecipientListGetEvent( + data.profileId, + data.teacherList.filter { it.loginId != null } + ) + + profile?.lastReceiversSync = System.currentTimeMillis() + + data.teacherOnConflictStrategy = OnConflictStrategy.REPLACE + EventBus.getDefault().postSticky(event) + onSuccess() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt new file mode 100644 index 00000000..1737187c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-1-2. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.base64Encode +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages +import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.getJsonObject +import pl.szczodrzynski.edziennik.getLong +import pl.szczodrzynski.edziennik.getString + +class LibrusMessagesSendMessage( + override val data: DataLibrus, + val recipients: List, + val subject: String, + val text: String, + val onSuccess: () -> Unit +) : LibrusMessages(data) { + companion object { + const val TAG = "LibrusMessages" + } + + init { + val params = mapOf( + "topic" to subject.base64Encode(), + "message" to text.base64Encode(), + "receivers" to recipients + .filter { it.loginId != null } + .joinToString(",") { it.loginId ?: "" }, + "actions" to "".base64Encode() + ) + + messagesGetJson(TAG, "SendMessage", parameters = params) { json -> + + val response = json.getJsonObject("response").getJsonObject("SendMessage") + val id = response.getLong("data") + + if (response.getString("status") != "ok" || id == null) { + val message = response.getString("message") + // TODO error + return@messagesGetJson + } + + LibrusMessagesGetList(data, type = Message.TYPE_SENT) { + val message = data.messageIgnoreList.firstOrNull { it.type == Message.TYPE_SENT && it.id == id } + val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id } + val event = MessageSentEvent(data.profileId, message, metadata?.addedDate) + + EventBus.getDefault().postSticky(event) + onSuccess() + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesTemplate.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesTemplate.kt new file mode 100644 index 00000000..797bb2ca --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesTemplate.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-25 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages + +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages + +class LibrusMessagesTemplate(override val data: DataLibrus, val onSuccess: () -> Unit) : LibrusMessages(data) { + companion object { + const val TAG = "LibrusMessages" + } + + init { + /* messagesGet(TAG, "") { doc -> + + data.setSyncNext(ENDPOINT_LIBRUS_MESSAGES_, SYNC_ALWAYS) + onSuccess() + } */ + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaHomework.kt new file mode 100644 index 00000000..ac6a173a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaHomework.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-22. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia + +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.HOUR +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOMEWORK +import pl.szczodrzynski.edziennik.data.api.POST +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.utils.models.Date + +class LibrusSynergiaHomework(override val data: DataLibrus, val onSuccess: () -> Unit) : LibrusSynergia(data) { + companion object { + const val TAG = "LibrusSynergiaHomework" + } + + init { data.profile?.also { profile -> + synergiaGet(TAG, "moje_zadania", method = POST, parameters = mapOf( + "dataOd" to + if (!data.profile.empty) + profile.getSemesterStart(1).stringY_m_d + else + Date.getToday().stringY_m_d, + "dataDo" to Date.getToday().stepForward(0, 0, 7).stringY_m_d, + "przedmiot" to -1 + + )) { text -> + val doc = Jsoup.parse(text) + + doc.select("table.myHomeworkTable > tbody").firstOrNull()?.also { homeworkTable -> + val homeworkElements = homeworkTable.children() + + val graphElements = doc.select("table[border].center td[align=left] tbody").first().children() + + homeworkElements.forEachIndexed { i, el -> + val elements = el.children() + + val subjectName = elements[0].text().trim() + val subjectId = data.subjectList.singleOrNull { it.longName == subjectName }?.id + ?: -1 + val teacherName = elements[1].text().trim() + val teacherId = data.teacherList.singleOrNull { teacherName == it.fullName }?.id + ?: -1 + val topic = elements[2].text().trim() + val addedDate = Date.fromY_m_d(elements[4].text().trim()).inMillis + val eventDate = Date.fromY_m_d(elements[6].text().trim()) + val id = "/podglad/([0-9]+)'".toRegex().find( + elements[9].select("input").attr("onclick") + )?.get(1)?.toLong() ?: return@forEachIndexed + + val lessons = data.db.timetableDao().getForDateNow(profileId, eventDate) + val startTime = lessons.firstOrNull { it.subjectId == subjectId }?.startTime + + val moreInfo = graphElements[2 * i + 1].select("td[title]") + .attr("title").trim() + val description = "Treść: (.*)".toRegex(RegexOption.DOT_MATCHES_ALL).find(moreInfo) + ?.get(1)?.replace("".toRegex(), "\n")?.trim() + + val seen = when (profile.empty) { + true -> true + else -> eventDate < Date.getToday() + } + + val eventObject = Event( + profileId, + id, + eventDate, + startTime, + "$topic\n$description", + -1, + Event.TYPE_HOMEWORK, + false, + teacherId, + subjectId, + data.teamClass?.id ?: -1 + ) + + data.eventList.add(eventObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_HOMEWORK, + id, + seen, + seen, + addedDate + )) + } + } + + data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_HOMEWORK)) + + // because this requires a synergia login (2 more requests) sync this every two hours or if explicit :D + data.setSyncNext(ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK, 2 * HOUR, DRAWER_ITEM_HOMEWORK) + onSuccess() + } + } ?: onSuccess() } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaInfo.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaInfo.kt new file mode 100644 index 00000000..3ccb230f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaInfo.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-23 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia + +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.MONTH +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_SYNERGIA_INFO +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia + +class LibrusSynergiaInfo(override val data: DataLibrus, val onSuccess: () -> Unit) : LibrusSynergia(data) { + companion object { + const val TAG = "LibrusSynergiaInfo" + } + + init { + synergiaGet(TAG, "informacja") { text -> + val doc = Jsoup.parse(text) + + doc.select("table.form tbody").firstOrNull()?.children()?.also { info -> + val studentNumber = info[2].select("td").text().trim().toIntOrNull() + + studentNumber?.also { + data.profile?.studentNumber = it + } + } + + data.setSyncNext(ENDPOINT_LIBRUS_SYNERGIA_INFO, MONTH) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaMarkAllAnnouncementsAsRead.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaMarkAllAnnouncementsAsRead.kt new file mode 100644 index 00000000..be807fe2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaMarkAllAnnouncementsAsRead.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-26 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia + +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata + +class LibrusSynergiaMarkAllAnnouncementsAsRead(override val data: DataLibrus, val onSuccess: () -> Unit) : LibrusSynergia(data) { + companion object { + const val TAG = "LibrusSynergiaMarkAllAnnouncementsAsRead" + } + + init { + synergiaGet(TAG, "ogloszenia") { + data.app.db.metadataDao().setAllSeen(profileId, Metadata.TYPE_ANNOUNCEMENT, true) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaTemplate.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaTemplate.kt new file mode 100644 index 00000000..af519128 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaTemplate.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-23 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia + +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia + +class LibrusSynergiaTemplate(override val data: DataLibrus, val onSuccess: () -> Unit) : LibrusSynergia(data) { + companion object { + const val TAG = "LibrusSynergia" + } + + init { + /* synergiaGet(TAG, "") { text -> + val doc = Jsoup.parse(text) + + data.setSyncNext(ENDPOINT_LIBRUS_SYNERGIA_, SYNC_ALWAYS) + onSuccess() + } */ + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt new file mode 100644 index 00000000..ea687d29 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt @@ -0,0 +1,117 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.firstlogin + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusPortal +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginApi +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginPortal +import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile + +class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "LibrusFirstLogin" + } + + private val portal = LibrusPortal(data) + private val api = LibrusApi(data) + private val profileList = mutableListOf() + + init { + if (data.loginStore.mode == LOGIN_MODE_LIBRUS_EMAIL) { + // email login: use Portal for account list + LibrusLoginPortal(data) { + portal.portalGet(TAG, if (data.fakeLogin) FAKE_LIBRUS_ACCOUNTS else LIBRUS_ACCOUNTS_URL) { json, response -> + val accounts = json.getJsonArray("accounts") + + if (accounts == null || accounts.size() < 1) { + data.error(ApiError(TAG, ERROR_NO_STUDENTS_IN_ACCOUNT) + .withResponse(response) + .withApiResponse(json)) + return@portalGet + } + val accountDataTime = json.getLong("lastModification") + + for (accountEl in accounts) { + val account = accountEl.asJsonObject + + val state = account.getString("state") + when (state) { + "requiring_an_action" -> ERROR_LIBRUS_PORTAL_SYNERGIA_DISCONNECTED + "need-activation" -> ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED + else -> null + }?.let { errorCode -> + data.error(ApiError(TAG, errorCode) + .withApiResponse(json) + .withResponse(response)) + return@portalGet + } + + val id = account.getInt("id") ?: continue + val login = account.getString("login") ?: continue + val token = account.getString("accessToken") ?: continue + val tokenTime = (accountDataTime ?: 0) + DAY + val name = account.getString("studentName")?.fixName() ?: "" + + val profile = Profile() + profile.studentNameLong = name + profile.studentNameShort = name.getShortName() + profile.name = profile.studentNameLong + profile.subname = data.portalEmail + profile.empty = true + profile.putStudentData("accountId", id) + profile.putStudentData("accountLogin", login) + profile.putStudentData("accountToken", token) + profile.putStudentData("accountTokenTime", tokenTime) + profileList.add(profile) + } + + EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore)) + onSuccess() + } + } + } + else { + // synergia or JST login: use Api for account info + LibrusLoginApi(data) { + api.apiGet(TAG, "Me") { json -> + + val profile = Profile() + + val me = json.getJsonObject("Me") + val account = me?.getJsonObject("Account") + val user = me?.getJsonObject("User") + + profile.putStudentData("isPremium", account?.getBoolean("IsPremium") == true || account?.getBoolean("IsPremiumDemo") == true) + + val isParent = account?.getInt("GroupId") == 5 + profile.accountNameLong = + if (isParent) + buildFullName(account?.getString("FirstName"), account?.getString("LastName")) + else null + + profile.studentNameLong = + buildFullName(user?.getString("FirstName"), user?.getString("LastName")) + + profile.studentNameShort = profile.studentNameLong?.getShortName() + profile.name = profile.studentNameLong + profile.subname = account.getString("Login") + profile.empty = true + profile.putStudentData("accountId", account.getInt("Id") ?: 0) + profile.putStudentData("accountLogin", profile.subname) + profile.putStudentData("accountToken", data.apiAccessToken) + profile.putStudentData("accountRefreshToken", data.apiRefreshToken) + profile.putStudentData("accountTokenTime", data.apiTokenExpiryTime) + profileList.add(profile) + + EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore)) + onSuccess() + } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLogin.kt new file mode 100644 index 00000000..c79acd2e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLogin.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.login + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_API +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_MESSAGES +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_PORTAL +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_SYNERGIA +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.utils.Utils + +class LibrusLogin(val data: DataLibrus, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "LibrusLogin" + } + + private var cancelled = false + + init { + nextLoginMethod(onSuccess) + } + + private fun nextLoginMethod(onSuccess: () -> Unit) { + if (data.targetLoginMethodIds.isEmpty()) { + onSuccess() + return + } + if (cancelled) { + onSuccess() + return + } + useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId -> + data.progress(data.progressStep) + if (usedMethodId != -1) + data.loginMethods.add(usedMethodId) + nextLoginMethod(onSuccess) + } + } + + private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) { + // this should never be true + if (data.loginMethods.contains(loginMethodId)) { + onSuccess(-1) + return + } + Utils.d(TAG, "Using login method $loginMethodId") + when (loginMethodId) { + LOGIN_METHOD_LIBRUS_PORTAL -> { + data.startProgress(R.string.edziennik_progress_login_librus_portal) + LibrusLoginPortal(data) { onSuccess(loginMethodId) } + } + LOGIN_METHOD_LIBRUS_API -> { + data.startProgress(R.string.edziennik_progress_login_librus_api) + LibrusLoginApi(data) { onSuccess(loginMethodId) } + } + LOGIN_METHOD_LIBRUS_SYNERGIA -> { + data.startProgress(R.string.edziennik_progress_login_librus_synergia) + LibrusLoginSynergia(data) { onSuccess(loginMethodId) } + } + LOGIN_METHOD_LIBRUS_MESSAGES -> { + data.startProgress(R.string.edziennik_progress_login_librus_messages) + LibrusLoginMessages(data) { onSuccess(loginMethodId) } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginApi.kt new file mode 100644 index 00000000..0f843a88 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginApi.kt @@ -0,0 +1,251 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-20. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.login + +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 pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.getInt +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.getUnixDate +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.net.HttpURLConnection.* + +class LibrusLoginApi { + companion object { + private const val TAG = "LoginLibrusApi" + } + + private lateinit var data: DataLibrus + private lateinit var onSuccess: () -> Unit + + /* do NOT move this to primary constructor */ + constructor(data: DataLibrus, onSuccess: () -> Unit) { + this.data = data + this.onSuccess = onSuccess + + if (data.loginStore.mode == LOGIN_MODE_LIBRUS_EMAIL && data.profile == null) { + data.error(ApiError(TAG, ERROR_PROFILE_MISSING)) + return + } + + if (data.isApiLoginValid()) { + onSuccess() + } + else { + when (data.loginStore.mode) { + LOGIN_MODE_LIBRUS_EMAIL -> loginWithPortal() + LOGIN_MODE_LIBRUS_SYNERGIA -> loginWithSynergia() + LOGIN_MODE_LIBRUS_JST -> loginWithJst() + else -> { + data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE)) + } + } + } + } + + private fun loginWithPortal() { + if (!data.loginMethods.contains(LOGIN_METHOD_LIBRUS_PORTAL)) { + data.error(ApiError(TAG, ERROR_LOGIN_METHOD_NOT_SATISFIED)) + return + } + SynergiaTokenExtractor(data) { + onSuccess() + } + } + + private fun copyFromLoginStore() { + data.loginStore.data?.apply { + if (has("accountLogin")) { + data.apiLogin = getString("accountLogin") + remove("accountLogin") + } + if (has("accountPassword")) { + data.apiPassword = getString("accountPassword") + remove("accountPassword") + } + if (has("accountCode")) { + data.apiCode = getString("accountCode") + remove("accountCode") + } + if (has("accountPin")) { + data.apiPin = getString("accountPin") + remove("accountPin") + } + } + } + + private fun loginWithSynergia() { + copyFromLoginStore() + if (data.apiRefreshToken != null) { + // refresh a Synergia token + synergiaRefreshToken() + } + else if (data.apiLogin != null && data.apiPassword != null) { + synergiaGetToken() + } + else { + // cannot log in: token expired, no login data present + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + } + } + + private fun loginWithJst() { + copyFromLoginStore() + + if (data.apiRefreshToken != null) { + // refresh a JST token + jstRefreshToken() + } + else if (data.apiCode != null && data.apiPin != null) { + // get a JST token from Code and PIN + jstGetToken() + } + else { + // cannot log in: token expired, no login data present + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + } + } + + private val tokenCallback = object : JsonCallbackHandler() { + override fun onSuccess(json: JsonObject?, response: Response?) { + if (response?.code() == HTTP_UNAVAILABLE) { + data.error(ApiError(TAG, ERROR_LIBRUS_API_MAINTENANCE) + .withApiResponse(json) + .withResponse(response)) + return + } + + if (json == null) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + if (response?.code() != 200) json.getString("error")?.let { error -> + when (error) { + "librus_captcha_needed" -> ERROR_LOGIN_LIBRUS_API_CAPTCHA_NEEDED + "connection_problems" -> ERROR_LOGIN_LIBRUS_API_CONNECTION_PROBLEMS + "invalid_client" -> ERROR_LOGIN_LIBRUS_API_INVALID_CLIENT + "librus_reg_accept_needed" -> ERROR_LOGIN_LIBRUS_API_REG_ACCEPT_NEEDED + "librus_change_password_error" -> ERROR_LOGIN_LIBRUS_API_CHANGE_PASSWORD_ERROR + "librus_password_change_required" -> ERROR_LOGIN_LIBRUS_API_PASSWORD_CHANGE_REQUIRED + "invalid_grant" -> ERROR_LOGIN_LIBRUS_API_INVALID_LOGIN + else -> ERROR_LOGIN_LIBRUS_API_OTHER + }.let { errorCode -> + data.error(ApiError(TAG, errorCode) + .withApiResponse(json) + .withResponse(response)) + return + } + } + + try { + data.apiAccessToken = json.getString("access_token") + data.apiRefreshToken = json.getString("refresh_token") + data.apiTokenExpiryTime = response.getUnixDate() + json.getInt("expires_in", 86400) + onSuccess() + } catch (e: NullPointerException) { + data.error(ApiError(TAG, EXCEPTION_LOGIN_LIBRUS_API_TOKEN) + .withResponse(response) + .withThrowable(e) + .withApiResponse(json)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + private fun synergiaGetToken() { + d(TAG, "Request: Librus/Login/Api - $LIBRUS_API_TOKEN_URL") + + Request.builder() + .url(LIBRUS_API_TOKEN_URL) + .userAgent(LIBRUS_USER_AGENT) + .addParameter("grant_type", "password") + .addParameter("username", data.apiLogin) + .addParameter("password", data.apiPassword) + .addParameter("librus_long_term_token", "1") + .addParameter("librus_rules_accepted", "1") + .addHeader("Authorization", "Basic $LIBRUS_API_AUTHORIZATION") + .contentType(MediaTypeUtils.APPLICATION_FORM) + .post() + .allowErrorCode(HTTP_BAD_REQUEST) + .allowErrorCode(HTTP_UNAUTHORIZED) + .allowErrorCode(HTTP_UNAVAILABLE) + .callback(tokenCallback) + .build() + .enqueue() + } + private fun synergiaRefreshToken() { + d(TAG, "Request: Librus/Login/Api - $LIBRUS_API_TOKEN_URL") + + Request.builder() + .url(LIBRUS_API_TOKEN_URL) + .userAgent(LIBRUS_USER_AGENT) + .addParameter("grant_type", "refresh_token") + .addParameter("refresh_token", data.apiRefreshToken) + .addParameter("librus_long_term_token", "1") + .addParameter("librus_rules_accepted", "1") + .addHeader("Authorization", "Basic $LIBRUS_API_AUTHORIZATION") + .contentType(MediaTypeUtils.APPLICATION_FORM) + .post() + .allowErrorCode(HTTP_BAD_REQUEST) + .allowErrorCode(HTTP_UNAUTHORIZED) + .callback(tokenCallback) + .build() + .enqueue() + } + private fun jstGetToken() { + d(TAG, "Request: Librus/Login/Api - $LIBRUS_API_TOKEN_JST_URL") + + Request.builder() + .url(LIBRUS_API_TOKEN_JST_URL) + .userAgent(LIBRUS_USER_AGENT) + .addParameter("grant_type", "implicit_grant") + .addParameter("client_id", LIBRUS_API_CLIENT_ID_JST) + .addParameter("secret", LIBRUS_API_SECRET_JST) + .addParameter("code", data.apiCode) + .addParameter("pin", data.apiPin) + .addParameter("librus_rules_accepted", "1") + .addParameter("librus_mobile_rules_accepted", "1") + .addParameter("librus_long_term_token", "1") + .contentType(MediaTypeUtils.APPLICATION_FORM) + .post() + .allowErrorCode(HTTP_BAD_REQUEST) + .allowErrorCode(HTTP_UNAUTHORIZED) + .callback(tokenCallback) + .build() + .enqueue() + } + private fun jstRefreshToken() { + d(TAG, "Request: Librus/Login/Api - $LIBRUS_API_TOKEN_JST_URL") + + Request.builder() + .url(LIBRUS_API_TOKEN_JST_URL) + .userAgent(LIBRUS_USER_AGENT) + .addParameter("grant_type", "refresh_token") + .addParameter("client_id", LIBRUS_API_CLIENT_ID_JST) + .addParameter("refresh_token", data.apiRefreshToken) + .addParameter("librus_long_term_token", "1") + .addParameter("mobile_app_accept_rules", "1") + .addParameter("synergy_accept_rules", "1") + .contentType(MediaTypeUtils.APPLICATION_FORM) + .post() + .allowErrorCode(HTTP_BAD_REQUEST) + .allowErrorCode(HTTP_UNAUTHORIZED) + .callback(tokenCallback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginMessages.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginMessages.kt new file mode 100644 index 00000000..9767683b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginMessages.kt @@ -0,0 +1,163 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-20. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.login + +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.body.MediaTypeUtils +import im.wangchao.mhttp.callback.TextCallbackHandler +import okhttp3.Cookie +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.getUnixDate +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.io.StringWriter +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + +class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "LoginLibrusMessages" + } + + private val callback by lazy { object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + val location = response?.headers()?.get("Location") + when { + location?.contains("MultiDomainLogon") == true -> loginWithSynergia(location) + location?.contains("AutoLogon") == true -> { + saveSessionId(response, text) + onSuccess() + } + + text?.contains("ok") == true -> { + saveSessionId(response, text) + onSuccess() + } + text?.contains("Niepoprawny login i/lub hasło.") == true -> data.error(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN, response, text) + text?.contains("stop.png") == true -> data.error(TAG, ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED, response, text) + text?.contains("eAccessDeny") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text) + text?.contains("OffLine") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_MAINTENANCE, response, text) + text?.contains("error") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ERROR, response, text) + text?.contains("eVarWhitThisNameNotExists") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text) + text?.contains("") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_OTHER, response, text) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + }} + + init { run { + if (data.profile == null) { + data.error(ApiError(TAG, ERROR_PROFILE_MISSING)) + return@run + } + + if (data.isMessagesLoginValid()) { + data.app.cookieJar.saveFromResponse(null, listOf( + Cookie.Builder() + .name("DZIENNIKSID") + .value(data.messagesSessionId!!) + .domain("wiadomosci.librus.pl") + .secure().httpOnly().build() + )) + onSuccess() + } + else { + data.app.cookieJar.clearForDomain("wiadomosci.librus.pl") + if (data.loginMethods.contains(LOGIN_METHOD_LIBRUS_SYNERGIA)) { + loginWithSynergia() + } + else if (data.apiLogin != null && data.apiPassword != null) { + loginWithCredentials() + } + else { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + } + } + }} + + /** + * XML (Flash messages website) login method. Uses a Synergia login and password. + */ + private fun loginWithCredentials() { + d(TAG, "Request: Librus/Login/Messages - $LIBRUS_MESSAGES_URL/Login") + + val docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder() + val doc = docBuilder.newDocument() + val serviceElement = doc.createElement("service") + val headerElement = doc.createElement("header") + val dataElement = doc.createElement("data") + val loginElement = doc.createElement("login") + loginElement.appendChild(doc.createTextNode(data.apiLogin)) + dataElement.appendChild(loginElement) + val passwordElement = doc.createElement("password") + passwordElement.appendChild(doc.createTextNode(data.apiPassword)) + dataElement.appendChild(passwordElement) + val keyStrokeElement = doc.createElement("KeyStroke") + val keysElement = doc.createElement("Keys") + val upElement = doc.createElement("Up") + keysElement.appendChild(upElement) + val downElement = doc.createElement("Down") + keysElement.appendChild(downElement) + keyStrokeElement.appendChild(keysElement) + dataElement.appendChild(keyStrokeElement) + serviceElement.appendChild(headerElement) + serviceElement.appendChild(dataElement) + doc.appendChild(serviceElement) + val transformer = TransformerFactory.newInstance().newTransformer() + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes") + val stringWriter = StringWriter() + transformer.transform(DOMSource(doc), StreamResult(stringWriter)) + val requestXml = stringWriter.toString() + + Request.builder() + .url("$LIBRUS_MESSAGES_URL/Login") + .userAgent(SYNERGIA_USER_AGENT) + .setTextBody(requestXml, MediaTypeUtils.APPLICATION_XML) + .post() + .callback(callback) + .build() + .enqueue() + } + + /** + * A login method using the Synergia website (/wiadomosci2 Auto Login). + */ + private fun loginWithSynergia(url: String = "https://synergia.librus.pl/wiadomosci2") { + d(TAG, "Request: Librus/Login/Messages - $url") + + Request.builder() + .url(url) + .userAgent(SYNERGIA_USER_AGENT) + .get() + .callback(callback) + .withClient(data.app.httpLazy) + .build() + .enqueue() + } + + private fun saveSessionId(response: Response?, text: String?) { + var sessionId = data.app.cookieJar.getCookie("wiadomosci.librus.pl", "DZIENNIKSID") + sessionId = sessionId?.replace("-MAINT", "") // dunno what's this + sessionId = sessionId?.replace("MAINT", "") // dunno what's this + if (sessionId == null) { + data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_NO_SESSION_ID) + .withResponse(response) + .withApiResponse(text)) + return + } + data.messagesSessionId = sessionId + data.messagesSessionIdExpiryTime = response.getUnixDate() + 45 * 60 /* 45min */ + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt new file mode 100644 index 00000000..41ad6255 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt @@ -0,0 +1,225 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.login + +import android.util.Pair +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 im.wangchao.mhttp.callback.TextCallbackHandler +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.getInt +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.getUnixDate +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.net.HttpURLConnection.HTTP_UNAUTHORIZED +import java.util.* +import java.util.regex.Pattern + +class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "LoginLibrusPortal" + } + + init { run { + if (data.loginStore.mode != LOGIN_MODE_LIBRUS_EMAIL) { + data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE)) + return@run + } + if (data.portalEmail == null || data.portalPassword == null) { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + return@run + } + + // succeed having a non-expired access token and a refresh token + if (data.isPortalLoginValid()) { + onSuccess() + } + else if (data.portalRefreshToken != null) { + data.app.cookieJar.clearForDomain("portal.librus.pl") + accessToken(null, data.portalRefreshToken) + } + else { + data.app.cookieJar.clearForDomain("portal.librus.pl") + authorize(if (data.fakeLogin) FAKE_LIBRUS_AUTHORIZE else LIBRUS_AUTHORIZE_URL) + } + }} + + private fun authorize(url: String?) { + d(TAG, "Request: Librus/Login/Portal - $url") + + Request.builder() + .url(url) + .userAgent(LIBRUS_USER_AGENT) + .withClient(data.app.httpLazy) + .callback(object : TextCallbackHandler() { + override fun onSuccess(json: String, response: Response) { + val location = response.headers().get("Location") + if (location != null) { + val authMatcher = Pattern.compile("http://localhost/bar\\?code=([A-z0-9]+?)$", Pattern.DOTALL or Pattern.MULTILINE).matcher(location) + if (authMatcher.find()) { + accessToken(authMatcher.group(1), null) + } else { + authorize(location) + } + } else { + val csrfMatcher = Pattern.compile("name=\"csrf-token\" content=\"([A-z0-9=+/\\-_]+?)\"", Pattern.DOTALL).matcher(json) + if (csrfMatcher.find()) { + login(csrfMatcher.group(1)) + } else { + data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_CSRF_MISSING) + .withResponse(response) + .withApiResponse(json)) + } + } + } + + override fun onFailure(response: Response, throwable: Throwable) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + }) + .build() + .enqueue() + } + + private fun login(csrfToken: String) { + d(TAG, "Request: Librus/Login/Portal - ${if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL}") + + Request.builder() + .url(if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL) + .userAgent(LIBRUS_USER_AGENT) + .addParameter("email", data.portalEmail) + .addParameter("password", data.portalPassword) + .addHeader("X-CSRF-TOKEN", csrfToken) + .contentType(MediaTypeUtils.APPLICATION_JSON) + .post() + .callback(object : JsonCallbackHandler() { + override fun onSuccess(json: JsonObject?, response: Response) { + val location = response.headers()?.get("Location") + if (location == "http://localhost/bar?command=close") { + data.error(ApiError(TAG, ERROR_LIBRUS_PORTAL_MAINTENANCE) + .withApiResponse(json) + .withResponse(response)) + return + } + + if (json == null) { + if (response.parserErrorBody?.contains("wciąż nieaktywne") == true) { + data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED) + .withResponse(response)) + return + } + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + if (json.get("errors") != null) { + data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR) + .withResponse(response) + .withApiResponse(json)) + return + } + authorize(json.getString("redirect", LIBRUS_AUTHORIZE_URL)) + } + + override fun onFailure(response: Response, throwable: Throwable) { + if (response.code() == 403 || response.code() == 401) { + data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN) + .withResponse(response) + .withThrowable(throwable)) + return + } + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + }) + .build() + .enqueue() + } + + private var refreshTokenFailed = false + private fun accessToken(code: String?, refreshToken: String?) { + d(TAG, "Request: Librus/Login/Portal - ${if (data.fakeLogin) FAKE_LIBRUS_TOKEN else LIBRUS_TOKEN_URL}") + + val onSuccess = { json: JsonObject, response: Response? -> + data.portalAccessToken = json.getString("access_token") + data.portalRefreshToken = json.getString("refresh_token") + data.portalTokenExpiryTime = response.getUnixDate() + json.getInt("expires_in", 86400) + onSuccess() + } + + val callback = object : JsonCallbackHandler() { + override fun onSuccess(json: JsonObject?, response: Response?) { + if (json == null) { + data.error(TAG, ERROR_RESPONSE_EMPTY, response) + return + } + val error = if (response?.code() == 200) null else + json.getString("hint") + error?.let { code -> + when (code) { + "Authorization code has expired" -> ERROR_LOGIN_LIBRUS_PORTAL_CODE_EXPIRED + "Authorization code has been revoked" -> ERROR_LOGIN_LIBRUS_PORTAL_CODE_REVOKED + "Cannot decrypt the refresh token" -> ERROR_LOGIN_LIBRUS_PORTAL_REFRESH_INVALID + "Token has been revoked" -> ERROR_LOGIN_LIBRUS_PORTAL_REFRESH_REVOKED + "Check the `client_id` parameter" -> ERROR_LOGIN_LIBRUS_PORTAL_NO_CLIENT_ID + "Check the `code` parameter" -> ERROR_LOGIN_LIBRUS_PORTAL_NO_CODE + "Check the `refresh_token` parameter" -> ERROR_LOGIN_LIBRUS_PORTAL_NO_REFRESH + "Check the `redirect_uri` parameter" -> ERROR_LOGIN_LIBRUS_PORTAL_NO_REDIRECT + else -> when (json.getString("error")) { + "unsupported_grant_type" -> ERROR_LOGIN_LIBRUS_PORTAL_UNSUPPORTED_GRANT + "invalid_client" -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_CLIENT_ID + else -> ERROR_LOGIN_LIBRUS_PORTAL_OTHER + } + }.let { errorCode -> + data.error(ApiError(TAG, errorCode) + .withApiResponse(json) + .withResponse(response)) + return + } + } + + try { + onSuccess(json, response) + } catch (e: NullPointerException) { + data.error(ApiError(TAG, EXCEPTION_LOGIN_LIBRUS_PORTAL_TOKEN) + .withResponse(response) + .withThrowable(e) + .withApiResponse(json)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + val params = ArrayList>() + params.add(Pair("client_id", LIBRUS_CLIENT_ID)) + if (code != null) { + params.add(Pair("grant_type", "authorization_code")) + params.add(Pair("code", code)) + params.add(Pair("redirect_uri", LIBRUS_REDIRECT_URL)) + } else if (refreshToken != null) { + params.add(Pair("grant_type", "refresh_token")) + params.add(Pair("refresh_token", refreshToken)) + } + + Request.builder() + .url(if (data.fakeLogin) FAKE_LIBRUS_TOKEN else LIBRUS_TOKEN_URL) + .userAgent(LIBRUS_USER_AGENT) + .addParams(params) + .post() + .allowErrorCode(HTTP_UNAUTHORIZED) + .callback(callback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginSynergia.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginSynergia.kt new file mode 100644 index 00000000..3e11d502 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginSynergia.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-20. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.login + +import com.google.gson.JsonObject +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.callback.TextCallbackHandler +import okhttp3.Cookie +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.getUnixDate +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.net.HttpURLConnection + +class LibrusLoginSynergia(override val data: DataLibrus, val onSuccess: () -> Unit) : LibrusApi(data) { + companion object { + private const val TAG = "LoginLibrusSynergia" + } + + init { run { + if (data.profile == null) { + data.error(ApiError(TAG, ERROR_PROFILE_MISSING)) + return@run + } + + if (data.isSynergiaLoginValid()) { + data.app.cookieJar.saveFromResponse(null, listOf( + Cookie.Builder() + .name("DZIENNIKSID") + .value(data.synergiaSessionId!!) + .domain("synergia.librus.pl") + .secure().httpOnly().build() + )) + onSuccess() + } + else { + data.app.cookieJar.clearForDomain("synergia.librus.pl") + if (data.loginMethods.contains(LOGIN_METHOD_LIBRUS_API)) { + loginWithApi() + } + else if (data.apiLogin != null && data.apiPassword != null && false) { + loginWithCredentials() + } + else { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + } + } + }} + + /** + * HTML form-based login method. Uses a Synergia login and password. + */ + private fun loginWithCredentials() { + + } + + /** + * A login method using the Synergia API (AutoLoginToken endpoint). + */ + private fun loginWithApi() { + d(TAG, "Request: Librus/Login/Synergia - $LIBRUS_API_URL/AutoLoginToken") + + val onSuccess = { json: JsonObject -> + loginWithToken(json.getString("Token")) + } + + apiGet(TAG, "AutoLoginToken", POST, null, onSuccess) + } + + private fun loginWithToken(token: String?) { + if (token == null) { + data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_SYNERGIA_NO_TOKEN)) + return + } + + d(TAG, "Request: Librus/Login/Synergia - " + LIBRUS_SYNERGIA_TOKEN_LOGIN_URL.replace("TOKEN", token) + "/uczen/widok/centrum_powiadomien") + + val callback = object : TextCallbackHandler() { + override fun onSuccess(json: String?, response: Response?) { + val location = response?.headers()?.get("Location") + if (location?.endsWith("przerwa_techniczna") == true) { + data.error(ApiError(TAG, ERROR_LIBRUS_SYNERGIA_MAINTENANCE) + .withApiResponse(json) + .withResponse(response)) + return + } + + if (location?.endsWith("centrum_powiadomien") == true) { + val sessionId = data.app.cookieJar.getCookie("synergia.librus.pl", "DZIENNIKSID") + if (sessionId == null) { + data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_SYNERGIA_NO_SESSION_ID) + .withResponse(response) + .withApiResponse(json)) + return + } + data.synergiaSessionId = sessionId + data.synergiaSessionIdExpiryTime = response.getUnixDate() + 45 * 60 /* 45min */ + onSuccess() + } + else { + data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_SYNERGIA_TOKEN_INVALID) + .withResponse(response) + .withApiResponse(json)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + data.app.cookieJar.clearForDomain("synergia.librus.pl") + Request.builder() + .url(LIBRUS_SYNERGIA_TOKEN_LOGIN_URL.replace("TOKEN", token) + "/uczen/widok/centrum_powiadomien") + .userAgent(LIBRUS_USER_AGENT) + .get() + .allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST) + .allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN) + .allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED) + .callback(callback) + .withClient(data.app.httpLazy) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/SynergiaTokenExtractor.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/SynergiaTokenExtractor.kt new file mode 100644 index 00000000..fb65518e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/SynergiaTokenExtractor.kt @@ -0,0 +1,73 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.librus.login + +import com.google.gson.JsonObject +import im.wangchao.mhttp.Response +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusPortal +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.utils.Utils.d + +class SynergiaTokenExtractor(override val data: DataLibrus, val onSuccess: () -> Unit) : LibrusPortal(data) { + companion object { + private const val TAG = "SynergiaTokenExtractor" + } + + init { run { + if (data.loginStore.mode != LOGIN_MODE_LIBRUS_EMAIL) { + data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE)) + return@run + } + if (data.profile == null) { + data.error(ApiError(TAG, ERROR_PROFILE_MISSING)) + return@run + } + + if (data.apiTokenExpiryTime-30 > currentTimeUnix() && data.apiAccessToken.isNotNullNorEmpty()) { + onSuccess() + } + else { + if (!synergiaAccount()) { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + } + } + }} + + /** + * Get an Api token from the Portal account, using Portal API. + * If necessary, refreshes the token. + */ + private fun synergiaAccount(): Boolean { + + val accountLogin = data.apiLogin ?: return false + data.portalAccessToken ?: return false + + d(TAG, "Request: Librus/SynergiaTokenExtractor - ${if (data.fakeLogin) FAKE_LIBRUS_ACCOUNT else LIBRUS_ACCOUNT_URL}$accountLogin") + + val onSuccess = { json: JsonObject, response: Response? -> + // synergiaAccount is executed when a synergia token needs a refresh + val accountId = json.getInt("id") + val accountToken = json.getString("accessToken") + if (accountId == null || accountToken == null) { + data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_SYNERGIA_TOKEN_MISSING) + .withResponse(response) + .withApiResponse(json)) + } + else { + data.apiAccessToken = accountToken + data.apiTokenExpiryTime = response.getUnixDate() + 6 * 60 * 60 + + // TODO remove this + data.profile?.studentNameLong = json.getString("studentName") ?: "" + val nameParts = json.getString("studentName")?.split(" ") + data.profile?.studentNameShort = nameParts?.get(0) + " " + nameParts?.get(1)?.get(0) + + onSuccess() + } + } + + portalGet(TAG, (if (data.fakeLogin) FAKE_LIBRUS_ACCOUNT else LIBRUS_ACCOUNT_URL)+accountLogin, onSuccess = onSuccess) + return true + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/DataMobidziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/DataMobidziennik.kt new file mode 100644 index 00000000..40ba688d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/DataMobidziennik.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-6. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik + +import android.util.LongSparseArray +import androidx.core.util.isNotEmpty +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_MOBIDZIENNIK_WEB +import pl.szczodrzynski.edziennik.data.api.models.Data +import pl.szczodrzynski.edziennik.currentTimeUnix +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { + + fun isWebLoginValid() = webSessionIdExpiryTime-30 > currentTimeUnix() + && webSessionValue.isNotNullNorEmpty() + && webSessionKey.isNotNullNorEmpty() + && webServerId.isNotNullNorEmpty() + + override fun satisfyLoginMethods() { + loginMethods.clear() + if (isWebLoginValid()) { + loginMethods += LOGIN_METHOD_MOBIDZIENNIK_WEB + } + } + + val teachersMap = LongSparseArray() + val subjectsMap = LongSparseArray() + + val gradeAddedDates = LongSparseArray() + val gradeAverages = LongSparseArray() + val gradeColors = LongSparseArray() + + private var mLoginServerName: String? = null + var loginServerName: String? + get() { mLoginServerName = mLoginServerName ?: loginStore.getLoginData("serverName", null); return mLoginServerName } + set(value) { loginStore.putLoginData("serverName", value); mLoginServerName = value } + + private var mLoginEmail: String? = null + var loginEmail: String? + get() { mLoginEmail = mLoginEmail ?: loginStore.getLoginData("email", null); return mLoginEmail } + set(value) { loginStore.putLoginData("email", value); mLoginEmail = value } + + private var mLoginUsername: String? = null + var loginUsername: String? + get() { mLoginUsername = mLoginUsername ?: loginStore.getLoginData("username", null); return mLoginUsername } + set(value) { loginStore.putLoginData("username", value); mLoginUsername = value } + + private var mLoginPassword: String? = null + var loginPassword: String? + get() { mLoginPassword = mLoginPassword ?: loginStore.getLoginData("password", null); return mLoginPassword } + set(value) { loginStore.putLoginData("password", value); mLoginPassword = value } + + private var mStudentId: Int? = null + var studentId: Int + get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 } + set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } + + /* __ __ _ + \ \ / / | | + \ \ /\ / /__| |__ + \ \/ \/ / _ \ '_ \ + \ /\ / __/ |_) | + \/ \/ \___|_._*/ + private var mWebSessionKey: String? = null + var webSessionKey: String? + get() { mWebSessionKey = mWebSessionKey ?: loginStore.getLoginData("sessionCookie", null); return mWebSessionKey } + set(value) { loginStore.putLoginData("sessionCookie", value); mWebSessionKey = value } + + private var mWebSessionValue: String? = null + var webSessionValue: String? + get() { mWebSessionValue = mWebSessionValue ?: loginStore.getLoginData("sessionID", null); return mWebSessionValue } + set(value) { loginStore.putLoginData("sessionID", value); mWebSessionValue = value } + + private var mWebServerId: String? = null + var webServerId: String? + get() { mWebServerId = mWebServerId ?: loginStore.getLoginData("sessionServer", null); return mWebServerId } + set(value) { loginStore.putLoginData("sessionServer", value); mWebServerId = value } + + private var mWebSessionIdExpiryTime: Long? = null + var webSessionIdExpiryTime: Long + get() { mWebSessionIdExpiryTime = mWebSessionIdExpiryTime ?: loginStore.getLoginData("sessionIDTime", 0L); return mWebSessionIdExpiryTime ?: 0L } + set(value) { loginStore.putLoginData("sessionIDTime", value); mWebSessionIdExpiryTime = value } + + + override fun saveData() { + super.saveData() + if (gradeAddedDates.isNotEmpty()) { + app.db.gradeDao().updateDetails(profileId, gradeAverages, gradeAddedDates, gradeColors) + } + } + + val mobiLessons = mutableListOf() + + data class MobiLesson( + var id: Long, + var subjectId: Long, + var teacherId: Long, + var teamId: Long, + var topic: String, + var date: Date, + var startTime: Time, + var endTime: Time, + var presentCount: Int, + var absentCount: Int, + var lessonNumber: Int, + var signed: String + ) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt new file mode 100644 index 00000000..08f1c293 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt @@ -0,0 +1,159 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikData +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebGetAttachment +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebGetMessage +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebGetRecipientList +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebSendMessage +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.firstlogin.MobidziennikFirstLogin +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLogin +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.utils.Utils.d + +class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface { + companion object { + private const val TAG = "Mobidziennik" + + const val API_KEY = "szkolny_eu_72c7dbc8b97f1e5dd2d118cacf51c2b8543d15c0f65b7a59979adb0a1296b235d7febb826dd2a28688def6efe0811b924b04d7f3c7b7d005354e06dc56815d57" + } + + val internalErrorList = mutableListOf() + val data: DataMobidziennik + private var afterLogin: (() -> Unit)? = null + + init { + data = DataMobidziennik(app, profile, loginStore).apply { + callback = wrapCallback(this@Mobidziennik.callback) + satisfyLoginMethods() + } + } + + private fun completed() { + data.saveData() + data.notify { + callback.onCompleted() + } + } + + /* _______ _ _ _ _ _ + |__ __| | /\ | | (_) | | | + | | | |__ ___ / \ | | __ _ ___ _ __ _| |_| |__ _ __ ___ + | | | '_ \ / _ \ / /\ \ | |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \ + | | | | | | __/ / ____ \| | (_| | (_) | | | | |_| | | | | | | | | + |_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| + __/ | + |__*/ + override fun sync(featureIds: List, viewId: Int?, arguments: JsonObject?) { + data.arguments = arguments + data.prepare(mobidziennikLoginMethods, MobidziennikFeatures, featureIds, viewId) + login() + } + + private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) { + d(TAG, "Trying to login with ${data.targetLoginMethodIds}") + if (internalErrorList.isNotEmpty()) { + d(TAG, " - Internal errors:") + internalErrorList.forEach { d(TAG, " - code $it") } + } + loginMethodId?.let { data.prepareFor(mobidziennikLoginMethods, it) } + afterLogin?.let { this.afterLogin = it } + MobidziennikLogin(data) { + data() + } + } + + private fun data() { + d(TAG, "Endpoint IDs: ${data.targetEndpointIds}") + if (internalErrorList.isNotEmpty()) { + d(TAG, " - Internal errors:") + internalErrorList.forEach { d(TAG, " - code $it") } + } + afterLogin?.invoke() ?: MobidziennikData(data) { + completed() + } + } + + override fun getMessage(message: MessageFull) { + login(LOGIN_METHOD_MOBIDZIENNIK_WEB) { + MobidziennikWebGetMessage(data, message) { + completed() + } + } + } + + override fun sendMessage(recipients: List, subject: String, text: String) { + login(LOGIN_METHOD_MOBIDZIENNIK_WEB) { + MobidziennikWebSendMessage(data, recipients, subject, text) { + completed() + } + } + } + + override fun markAllAnnouncementsAsRead() {} + override fun getAnnouncement(announcement: AnnouncementFull) {} + + override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) { + login(LOGIN_METHOD_MOBIDZIENNIK_WEB) { + MobidziennikWebGetAttachment(data, message, attachmentId, attachmentName) { + completed() + } + } + } + + override fun getRecipientList() { + login(LOGIN_METHOD_MOBIDZIENNIK_WEB) { + MobidziennikWebGetRecipientList(data) { + completed() + } + } + } + + override fun firstLogin() { MobidziennikFirstLogin(data) { completed() } } + override fun cancel() { + d(TAG, "Cancelled") + data.cancel() + } + + private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { + return object : EdziennikCallback { + override fun onCompleted() { callback.onCompleted() } + override fun onProgress(step: Float) { callback.onProgress(step) } + override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) } + override fun onError(apiError: ApiError) { + if (apiError.errorCode in internalErrorList) { + // finish immediately if the same error occurs twice during the same sync + callback.onError(apiError) + return + } + internalErrorList.add(apiError.errorCode) + when (apiError.errorCode) { + ERROR_MOBIDZIENNIK_WEB_ACCESS_DENIED, + ERROR_MOBIDZIENNIK_WEB_NO_SESSION_KEY, + ERROR_MOBIDZIENNIK_WEB_NO_SESSION_VALUE, + ERROR_MOBIDZIENNIK_WEB_NO_SERVER_ID -> { + data.loginMethods.remove(LOGIN_METHOD_MOBIDZIENNIK_WEB) + data.prepareFor(mobidziennikLoginMethods, LOGIN_METHOD_MOBIDZIENNIK_WEB) + data.webSessionIdExpiryTime = 0 + login() + } + else -> callback.onError(apiError) + } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt new file mode 100644 index 00000000..c08846c9 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik + +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.models.Feature + +const val ENDPOINT_MOBIDZIENNIK_API_MAIN = 1000 +const val ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_INBOX = 2011 +const val ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_SENT = 2012 +const val ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL = 2019 +const val ENDPOINT_MOBIDZIENNIK_WEB_CALENDAR = 2020 +const val ENDPOINT_MOBIDZIENNIK_WEB_GRADES = 2030 +const val ENDPOINT_MOBIDZIENNIK_WEB_NOTICES = 2040 +const val ENDPOINT_MOBIDZIENNIK_WEB_ATTENDANCE = 2050 +const val ENDPOINT_MOBIDZIENNIK_WEB_MANUALS = 2100 +const val ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL = 2200 +const val ENDPOINT_MOBIDZIENNIK_API2_MAIN = 3000 + +val MobidziennikFeatures = listOf( + // always synced + Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_ALWAYS_NEEDED, listOf( + ENDPOINT_MOBIDZIENNIK_API_MAIN to LOGIN_METHOD_MOBIDZIENNIK_WEB, + ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL to LOGIN_METHOD_MOBIDZIENNIK_WEB + ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)), // TODO divide features into separate view IDs (all with API_MAIN) + + // push config + /*Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_PUSH_CONFIG, listOf( + ENDPOINT_MOBIDZIENNIK_API2_MAIN to LOGIN_METHOD_MOBIDZIENNIK_API2 + ), listOf(LOGIN_METHOD_MOBIDZIENNIK_API2)).withShouldSync { data -> + data.app.appConfig.fcmTokens[LOGIN_TYPE_MOBIDZIENNIK]?.second?.contains(data.profileId) == false + },*/ + + + + + + /** + * Agenda - "API" + web scraping. + */ + Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_AGENDA, listOf( + ENDPOINT_MOBIDZIENNIK_API_MAIN to LOGIN_METHOD_MOBIDZIENNIK_WEB, + ENDPOINT_MOBIDZIENNIK_WEB_CALENDAR to LOGIN_METHOD_MOBIDZIENNIK_WEB + ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB, LOGIN_METHOD_MOBIDZIENNIK_WEB)), + /** + * Grades - "API" + web scraping. + */ + Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_GRADES, listOf( + ENDPOINT_MOBIDZIENNIK_API_MAIN to LOGIN_METHOD_MOBIDZIENNIK_WEB, + ENDPOINT_MOBIDZIENNIK_WEB_GRADES to LOGIN_METHOD_MOBIDZIENNIK_WEB + ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB, LOGIN_METHOD_MOBIDZIENNIK_WEB)), + /** + * Behaviour - "API" + web scraping. + */ + Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_BEHAVIOUR, listOf( + ENDPOINT_MOBIDZIENNIK_API_MAIN to LOGIN_METHOD_MOBIDZIENNIK_WEB, + ENDPOINT_MOBIDZIENNIK_WEB_NOTICES to LOGIN_METHOD_MOBIDZIENNIK_WEB + ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB, LOGIN_METHOD_MOBIDZIENNIK_WEB)), + // attendance TODO implement website attendance scraping + /*Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_ATTENDANCE, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_ATTENDANCE to LOGIN_METHOD_MOBIDZIENNIK_WEB + ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)),*/ + + + + + + /** + * Messages inbox - using web scraper. + */ + Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_MESSAGES_INBOX, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_INBOX to LOGIN_METHOD_MOBIDZIENNIK_WEB, + ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL to LOGIN_METHOD_MOBIDZIENNIK_WEB + ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)), + /** + * Messages sent - using web scraper. + */ + Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_MESSAGES_SENT, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_SENT to LOGIN_METHOD_MOBIDZIENNIK_WEB, + ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL to LOGIN_METHOD_MOBIDZIENNIK_WEB + ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)) + + // lucky number possibilities + // all endpoints that may supply the lucky number + /*Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_LUCKY_NUMBER, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_MANUALS to LOGIN_METHOD_MOBIDZIENNIK_WEB + ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)).apply { priority = 10 }, + + Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_LUCKY_NUMBER, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_CALENDAR to LOGIN_METHOD_MOBIDZIENNIK_WEB + ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)).apply { priority = 3 }, + + Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_LUCKY_NUMBER, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_GRADES to LOGIN_METHOD_MOBIDZIENNIK_WEB + ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)).apply { priority = 2 }, + + Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_LUCKY_NUMBER, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_NOTICES to LOGIN_METHOD_MOBIDZIENNIK_WEB + ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)).apply { priority = 1 }, + + Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_LUCKY_NUMBER, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_ATTENDANCE to LOGIN_METHOD_MOBIDZIENNIK_WEB + ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)).apply { priority = 4 }*/ + +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/MobidziennikData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/MobidziennikData.kt new file mode 100644 index 00000000..0ceeb49a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/MobidziennikData.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.* +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api.MobidziennikApi +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebCalendar +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebGrades +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebMessagesAll +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebMessagesInbox +import pl.szczodrzynski.edziennik.utils.Utils + +class MobidziennikData(val data: DataMobidziennik, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "MobidziennikData" + } + + init { + nextEndpoint(onSuccess) + } + + private fun nextEndpoint(onSuccess: () -> Unit) { + if (data.targetEndpointIds.isEmpty()) { + onSuccess() + return + } + if (data.cancelled) { + onSuccess() + return + } + useEndpoint(data.targetEndpointIds.removeAt(0)) { + data.progress(data.progressStep) + nextEndpoint(onSuccess) + } + } + + private fun useEndpoint(endpointId: Int, onSuccess: () -> Unit) { + Utils.d(TAG, "Using endpoint $endpointId") + when (endpointId) { + ENDPOINT_MOBIDZIENNIK_API_MAIN -> { + data.startProgress(R.string.edziennik_progress_endpoint_data) + MobidziennikApi(data, onSuccess) + } + ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_INBOX -> { + data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox) + MobidziennikWebMessagesInbox(data) { onSuccess() } + } + ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL -> { + data.startProgress(R.string.edziennik_progress_endpoint_messages) + MobidziennikWebMessagesAll(data) { onSuccess() } + } + ENDPOINT_MOBIDZIENNIK_WEB_CALENDAR -> { + data.startProgress(R.string.edziennik_progress_endpoint_calendar) + MobidziennikWebCalendar(data) { onSuccess() } + } + ENDPOINT_MOBIDZIENNIK_WEB_GRADES -> { + data.startProgress(R.string.edziennik_progress_endpoint_grades) + MobidziennikWebGrades(data) { onSuccess() } + }/* + ENDPOINT_MOBIDZIENNIK_WEB_NOTICES -> { + data.startProgress(R.string.edziennik_progress_endpoint_behaviour) + MobidziennikWebNotices(data) { onSuccess() } + } + ENDPOINT_MOBIDZIENNIK_WEB_ATTENDANCE -> { + data.startProgress(R.string.edziennik_progress_endpoint_attendance) + MobidziennikWebAttendance(data) { onSuccess() } + } + ENDPOINT_MOBIDZIENNIK_WEB_MANUALS -> { + data.startProgress(R.string.edziennik_progress_endpoint_lucky_number) + MobidziennikWebManuals(data) { onSuccess() } + }*/ + else -> onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/MobidziennikWeb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/MobidziennikWeb.kt new file mode 100644 index 00000000..721d3090 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/MobidziennikWeb.kt @@ -0,0 +1,187 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data + +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.callback.FileCallbackHandler +import im.wangchao.mhttp.callback.TextCallbackHandler +import okhttp3.Cookie +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.io.File + +open class MobidziennikWeb(open val data: DataMobidziennik) { + companion object { + private const val TAG = "MobidziennikWeb" + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + fun webGet( + tag: String, + endpoint: String, + method: Int = GET, + parameters: List> = emptyList(), + fullUrl: String? = null, + onSuccess: (text: String) -> Unit + ) { + val url = fullUrl ?: "https://${data.loginServerName}.mobidziennik.pl$endpoint" + + d(tag, "Request: Mobidziennik/Web - $url") + + if (data.webSessionKey == null) { + data.error(TAG, ERROR_MOBIDZIENNIK_WEB_NO_SESSION_KEY) + return + } + if (data.webSessionValue == null) { + data.error(TAG, ERROR_MOBIDZIENNIK_WEB_NO_SESSION_VALUE) + return + } + if (data.webServerId == null) { + data.error(TAG, ERROR_MOBIDZIENNIK_WEB_NO_SERVER_ID) + return + } + + val callback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + if (text.isNullOrEmpty()) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + if (text == "Nie jestes zalogowany" + || text.contains("przypomnij_haslo_email")) { + data.error(ApiError(TAG, ERROR_MOBIDZIENNIK_WEB_ACCESS_DENIED) + .withResponse(response)) + return + } + + try { + onSuccess(text) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_MOBIDZIENNIK_WEB_REQUEST) + .withResponse(response) + .withThrowable(e) + .withApiResponse(text)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + data.app.cookieJar.saveFromResponse(null, listOf( + Cookie.Builder() + .name(data.webSessionKey!!) + .value(data.webSessionValue!!) + .domain("${data.loginServerName}.mobidziennik.pl") + .secure().httpOnly().build(), + Cookie.Builder() + .name("SERVERID") + .value(data.webServerId!!) + .domain("${data.loginServerName}.mobidziennik.pl") + .secure().httpOnly().build() + )) + + Request.builder() + .url(url) + .userAgent(MOBIDZIENNIK_USER_AGENT) + .apply { + when (method) { + GET -> get() + POST -> post() + } + parameters.map { (name, value) -> + addParameter(name, value) + } + } + .callback(callback) + .build() + .enqueue() + } + + fun webGetFile(tag: String, action: String, targetFile: File, onSuccess: (file: File) -> Unit, + onProgress: (written: Long, total: Long) -> Unit) { + val url = "https://${data.loginServerName}.mobidziennik.pl$action" + + d(tag, "Request: Mobidziennik/Web - $url") + + if (data.webSessionKey == null) { + data.error(TAG, ERROR_MOBIDZIENNIK_WEB_NO_SESSION_KEY) + return + } + if (data.webSessionValue == null) { + data.error(TAG, ERROR_MOBIDZIENNIK_WEB_NO_SESSION_VALUE) + return + } + if (data.webServerId == null) { + data.error(TAG, ERROR_MOBIDZIENNIK_WEB_NO_SERVER_ID) + return + } + + val callback = object : FileCallbackHandler(targetFile) { + override fun onSuccess(file: File?, response: Response?) { + if (file == null) { + data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD) + .withResponse(response)) + return + } + + try { + onSuccess(file) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_MOBIDZIENNIK_WEB_FILE_REQUEST) + .withResponse(response) + .withThrowable(e)) + } + } + + override fun onProgress(bytesWritten: Long, bytesTotal: Long) { + try { + onProgress(bytesWritten, bytesTotal) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_MOBIDZIENNIK_WEB_FILE_REQUEST) + .withThrowable(e)) + } + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + data.app.cookieJar.saveFromResponse(null, listOf( + Cookie.Builder() + .name(data.webSessionKey!!) + .value(data.webSessionValue!!) + .domain("${data.loginServerName}.mobidziennik.pl") + .secure().httpOnly().build(), + Cookie.Builder() + .name("SERVERID") + .value(data.webServerId!!) + .domain("${data.loginServerName}.mobidziennik.pl") + .secure().httpOnly().build() + )) + + Request.builder() + .url(url) + .userAgent(MOBIDZIENNIK_USER_AGENT) + .callback(callback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApi.kt new file mode 100644 index 00000000..543f21dc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApi.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api + +import pl.szczodrzynski.edziennik.data.api.ERROR_MOBIDZIENNIK_WEB_INVALID_RESPONSE +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_API_MAIN +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS + +class MobidziennikApi(override val data: DataMobidziennik, + val onSuccess: () -> Unit) : MobidziennikWeb(data) { + companion object { + private const val TAG = "MobidziennikApi" + } + + init { + webGet(TAG, "/api/zrzutbazy") { text -> + if (!text.contains("T@B#LA")) { + data.error(ApiError(TAG, ERROR_MOBIDZIENNIK_WEB_INVALID_RESPONSE) + .withApiResponse(text)) + return@webGet + } + + val tables = text.split("T@B#LA") + tables.forEachIndexed { index, table -> + val rows = table.split("\n") + when (index) { + 0 -> MobidziennikApiUsers(data, rows) + 3 -> MobidziennikApiDates(data, rows) + 4 -> MobidziennikApiSubjects(data, rows) + 7 -> MobidziennikApiTeams(data, rows, null) + 8 -> MobidziennikApiStudent(data, rows) + 9 -> MobidziennikApiTeams(data, null, rows) + 14 -> MobidziennikApiGradeCategories(data, rows) + 15 -> MobidziennikApiLessons(data, rows) + 16 -> MobidziennikApiAttendance(data, rows) + 17 -> MobidziennikApiNotices(data, rows) + 18 -> MobidziennikApiGrades(data, rows) + 21 -> MobidziennikApiEvents(data, rows) + 23 -> MobidziennikApiHomework(data, rows) + 24 -> MobidziennikApiTimetable(data, rows) + } + } + + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_API_MAIN, SYNC_ALWAYS) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiAttendance.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiAttendance.kt new file mode 100644 index 00000000..76369615 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiAttendance.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api + +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance +import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.* +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata + +class MobidziennikApiAttendance(val data: DataMobidziennik, rows: List) { + init { run { + for (row in rows) { + if (row.isEmpty()) + continue + val cols = row.split("|") + + val studentId = cols[2].toInt() + if (studentId != data.studentId) + return@run + + val id = cols[0].toLong() + val lessonId = cols[1].toLong() + data.mobiLessons.singleOrNull { it.id == lessonId }?.let { lesson -> + val type = when (cols[4]) { + "2" -> TYPE_ABSENT + "5" -> TYPE_ABSENT_EXCUSED + "4" -> TYPE_RELEASED + else -> TYPE_PRESENT + } + val semester = data.profile?.dateToSemester(lesson.date) ?: 1 + + val attendanceObject = Attendance( + data.profileId, + id, + lesson.teacherId, + lesson.subjectId, + semester, + lesson.topic, + lesson.date, + lesson.startTime, + type) + + data.attendanceList.add(attendanceObject) + data.metadataList.add( + Metadata( + data.profileId, + Metadata.TYPE_ATTENDANCE, + id, + data.profile?.empty ?: false, + data.profile?.empty ?: false, + System.currentTimeMillis() + )) + } + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiDates.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiDates.kt new file mode 100644 index 00000000..a0b3dbc2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiDates.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api + +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.utils.models.Date + +class MobidziennikApiDates(val data: DataMobidziennik, rows: List) { + init { + for (row in rows) { + if (row.isEmpty()) + continue + val cols = row.split("|") + + when (cols[1]) { + "semestr1_poczatek" -> data.profile?.dateSemester1Start = Date.fromYmd(cols[3]) + "semestr2_poczatek" -> data.profile?.dateSemester2Start = Date.fromYmd(cols[3]) + "koniec_roku_szkolnego" -> data.profile?.dateYearEnd = Date.fromYmd(cols[3]) + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiEvents.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiEvents.kt new file mode 100644 index 00000000..adfdee11 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiEvents.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api + +import androidx.core.util.contains +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + +class MobidziennikApiEvents(val data: DataMobidziennik, rows: List) { + init { + for (row in rows) { + if (row.isEmpty()) + continue + val cols = row.split("|") + + val teamId = cols[2].toLong() + if (data.teamList.contains(teamId)) { + + val id = cols[0].toLong() + val teacherId = cols[1].toLong() + val subjectId = cols[3].toLong() + var type = Event.TYPE_DEFAULT + var topic = cols[5] + Regexes.MOBIDZIENNIK_EVENT_TYPE.find(topic)?.let { + val typeText = it.groupValues[1] + when (typeText) { + "sprawdzian" -> type = Event.TYPE_EXAM + "kartkówka" -> type = Event.TYPE_SHORT_QUIZ + } + topic = topic.replace("($typeText)", "").trim() + } + val eventDate = Date.fromYmd(cols[4]) + val startTime = Time.fromYmdHm(cols[6]) + val format = SimpleDateFormat("yyyyMMddHHmmss", Locale.US) + val addedDate = try { + format.parse(cols[7]).time + } catch (e: ParseException) { + e.printStackTrace() + System.currentTimeMillis() + } + + + val eventObject = Event( + data.profileId, + id, + eventDate, + startTime, + topic, + -1, + type, + false, + teacherId, + subjectId, + teamId) + + data.eventList.add(eventObject) + data.metadataList.add( + Metadata( + data.profileId, + Metadata.TYPE_EVENT, + id, + data.profile?.empty ?: false, + data.profile?.empty ?: false, + addedDate + )) + } + } + + data.toRemove.add(DataRemoveModel.Events.futureExceptType(Event.TYPE_HOMEWORK)) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiGradeCategories.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiGradeCategories.kt new file mode 100644 index 00000000..87b069cc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiGradeCategories.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-7. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api + +import android.graphics.Color +import androidx.core.util.contains +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory + +class MobidziennikApiGradeCategories(val data: DataMobidziennik, rows: List) { + init { + for (row in rows) { + if (row.isEmpty()) + continue + val cols = row.split("|") + + val teamId = cols[1].toLong() + if (data.teamList.contains(teamId)) { + + val id = cols[0].toLong() + val weight = cols[3].toFloat() + val color = Color.parseColor("#" + cols[6]) + val category = cols[4] + val columns = cols[7].split(";") + + data.gradeCategories.put( + id, + GradeCategory( + data.profileId, + id, + weight, + color, + category + ).addColumns(columns) + ) + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiGrades.kt new file mode 100644 index 00000000..61518aa3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiGrades.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-8. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api + +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade.* +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata + +class MobidziennikApiGrades(val data: DataMobidziennik, rows: List) { + init { data.profile?.also { profile -> run { + data.db.gradeDao().getDetails( + data.profileId, + data.gradeAddedDates, + data.gradeAverages, + data.gradeColors + ) + var addedDate = System.currentTimeMillis() + for (row in rows) { + if (row.isEmpty()) + continue + val cols = row.split("|") + + val studentId = cols[1].toInt() + if (studentId != data.studentId) + return@run + + val id = cols[0].toLong() + val categoryId = cols[6].toLongOrNull() ?: -1 + val categoryColumn = cols[10].toIntOrNull() ?: 1 + val name = cols[7] + val value = cols[11].toFloat() + val semester = cols[5].toInt() + val teacherId = cols[2].toLong() + val subjectId = cols[3].toLong() + val type = when (cols[8]) { + "3" -> if (semester == 1) TYPE_SEMESTER1_PROPOSED else TYPE_SEMESTER2_PROPOSED + "1" -> if (semester == 1) TYPE_SEMESTER1_FINAL else TYPE_SEMESTER2_FINAL + "4" -> TYPE_YEAR_PROPOSED + "2" -> TYPE_YEAR_FINAL + else -> TYPE_NORMAL + } + + var weight = 0.0f + var category = "" + var description = "" + var color = -1 + data.gradeCategories.get(categoryId)?.let { gradeCategory -> + weight = gradeCategory.weight + category = gradeCategory.text + description = gradeCategory.columns[categoryColumn-1] + color = gradeCategory.color + } + + // fix for "0" value grades, so they're not counted in the average + if (value == 0.0f/* && data.app.appConfig.dontCountZeroToAverage*/) { + weight = 0.0f + } + + val gradeObject = Grade( + data.profileId, + id, + category, + color, + description, + name, + value, + weight, + semester, + teacherId, + subjectId) + gradeObject.type = type + + data.toRemove.addAll(listOf( + TYPE_NORMAL, + TYPE_SEMESTER1_FINAL, + TYPE_SEMESTER2_FINAL, + TYPE_SEMESTER1_PROPOSED, + TYPE_SEMESTER2_PROPOSED, + TYPE_YEAR_FINAL, + TYPE_YEAR_PROPOSED + ).map { + DataRemoveModel.Grades.semesterWithType(profile.currentSemester, it) + }) + data.gradeList.add(gradeObject) + data.metadataList.add( + Metadata( + data.profileId, + Metadata.TYPE_GRADE, + id, + data.profile?.empty ?: false, + data.profile?.empty ?: false, + addedDate + )) + addedDate++ + } + }}} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt new file mode 100644 index 00000000..37605401 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api + +import androidx.core.util.contains +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +class MobidziennikApiHomework(val data: DataMobidziennik, rows: List) { + init { + for (row in rows) { + if (row.isEmpty()) + continue + val cols = row.split("|") + + val teamId = cols[5].toLong() + if (data.teamList.contains(teamId)) { + + val id = cols[0].toLong() + val teacherId = cols[7].toLong() + val subjectId = cols[6].toLong() + val topic = cols[1] + val eventDate = Date.fromYmd(cols[2]) + val startTime = Time.fromYmdHm(cols[3]) + + val eventObject = Event( + data.profileId, + id, + eventDate, + startTime, + topic, + -1, + Event.TYPE_HOMEWORK, + false, + teacherId, + subjectId, + teamId) + + data.eventList.add(eventObject) + data.metadataList.add( + Metadata( + data.profileId, + Metadata.TYPE_HOMEWORK, + id, + data.profile?.empty ?: false, + data.profile?.empty ?: false, + System.currentTimeMillis() + )) + } + } + + data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_HOMEWORK)) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiLessons.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiLessons.kt new file mode 100644 index 00000000..47ab7494 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiLessons.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-7. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api + +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +class MobidziennikApiLessons(val data: DataMobidziennik, rows: List) { + init { + data.mobiLessons.clear() + for (row in rows) { + if (row.isEmpty()) + continue + val cols = row.split("|") + + val id = cols[0].toLong() + val subjectId = cols[1].toLong() + val teacherId = cols[2].toLong() + val teamId = cols[3].toLong() + val topic = cols[4] + val date = Date.fromYmd(cols[5]) + val startTime = Time.fromYmdHm(cols[6]) + val endTime = Time.fromYmdHm(cols[7]) + val presentCount = cols[8].toInt() + val absentCount = cols[9].toInt() + val lessonNumber = cols[10].toInt() + val signed = cols[11] + + val lesson = DataMobidziennik.MobiLesson( + id, + subjectId, + teacherId, + teamId, + topic, + date, + startTime, + endTime, + presentCount, + absentCount, + lessonNumber, + signed + ) + data.mobiLessons.add(lesson) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiNotices.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiNotices.kt new file mode 100644 index 00000000..5520f490 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiNotices.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api + +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.notices.Notice +import pl.szczodrzynski.edziennik.utils.models.Date + +class MobidziennikApiNotices(val data: DataMobidziennik, rows: List) { + init { run { + for (row in rows) { + if (row.isEmpty()) + continue + val cols = row.split("|") + + val studentId = cols[2].toInt() + if (studentId != data.studentId) + return@run + + val id = cols[0].toLong() + val text = cols[4] + val semester = cols[6].toInt() + val type = when (cols[3]) { + "0" -> Notice.TYPE_NEGATIVE + "1" -> Notice.TYPE_POSITIVE + "3" -> Notice.TYPE_NEUTRAL + else -> Notice.TYPE_NEUTRAL + } + val teacherId = cols[5].toLong() + val addedDate = Date.fromYmd(cols[7]).inMillis + + val noticeObject = Notice( + data.profileId, + id, + text, + semester, + type, + teacherId) + + data.noticeList.add(noticeObject) + data.metadataList.add( + Metadata( + data.profileId, + Metadata.TYPE_NOTICE, + id, + data.profile?.empty ?: false, + data.profile?.empty ?: false, + addedDate + )) + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiStudent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiStudent.kt new file mode 100644 index 00000000..2435478e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiStudent.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-6. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api + +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik + +class MobidziennikApiStudent(val data: DataMobidziennik, rows: List) { + init { run { + if (rows.size < 2) { + return@run + } + + val student1 = rows[0].split("|") + val student2 = rows[1].split("|") + + // FROM OLD Mobidziennik API - this information seems to be unused + /*students.clear(); + String[] student = table.split("\n"); + for (int i = 0; i < student.length; i++) { + if (student[i].isEmpty()) { + continue; + } + String[] student1 = student[i].split("\\|", Integer.MAX_VALUE); + String[] student2 = student[++i].split("\\|", Integer.MAX_VALUE); + students.put(strToInt(student1[0]), new Pair<>(student1, student2)); + } + Pair studentData = students.get(studentId); + try { + profile.setAttendancePercentage(Float.parseFloat(studentData.second[1])); + } + catch (Exception e) { + e.printStackTrace(); + }*/ + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiSubjects.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiSubjects.kt new file mode 100644 index 00000000..eb2cef64 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiSubjects.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-6. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api + +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject + +class MobidziennikApiSubjects(val data: DataMobidziennik, rows: List) { + init { + for (row in rows) { + if (row.isEmpty()) + continue + val cols = row.split("|") + + val id = cols[0].toLong() + val longName = cols[1].trim() + val shortName = cols[2].trim() + + data.subjectsMap.put(id, longName) + data.subjectList.put(id, Subject(data.profileId, id, longName, shortName)) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTeams.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTeams.kt new file mode 100644 index 00000000..309e9cdb --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTeams.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api + +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.db.modules.teams.Team +import pl.szczodrzynski.edziennik.getById +import pl.szczodrzynski.edziennik.values + +class MobidziennikApiTeams(val data: DataMobidziennik, tableTeams: List?, tableRelations: List?) { + init { + if (tableTeams != null) { + for (row in tableTeams) { + if (row.isEmpty()) + continue + val cols = row.split("|") + + val id = cols[0].toLong() + val name = cols[1]+cols[2] + val type = cols[3].toInt() + val code = data.loginServerName+":"+name + val teacherId = cols[4].toLongOrNull() ?: -1 + + val teamObject = Team( + data.profileId, + id, + name, + type, + code, + teacherId) + data.teamList.put(id, teamObject) + } + } + if (tableRelations != null) { + val allTeams = data.teamList.values() + data.teamList.clear() + + for (row in tableRelations) { + if (row.isEmpty()) + continue + val cols = row.split("|") + + val studentId = cols[1].toInt() + val teamId = cols[2].toLong() + val studentNumber = cols[4].toInt() + + if (studentId != data.studentId) + continue + val team = allTeams.getById(teamId) + if (team != null) { + if (team.type == 1) { + data.profile?.studentNumber = studentNumber + data.teamClass = team + } + data.teamList.put(teamId, team) + } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTimetable.kt new file mode 100644 index 00000000..1832e2fe --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTimetable.kt @@ -0,0 +1,201 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api + +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson +import pl.szczodrzynski.edziennik.fixName +import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List) { + init { data.profile?.also { profile -> + val lessons = rows.filterNot { it.isEmpty() }.map { it.split("|") } + + val dataStart = Date.getToday() + val dataEnd = dataStart.clone().stepForward(0, 0, 7 + (6 - dataStart.weekDay)) + + data.toRemove.add(DataRemoveModel.Timetable.between(dataStart.clone(), dataEnd)) + + val dataDays = mutableListOf() + while (dataStart <= dataEnd) { + dataDays += dataStart.value + dataStart.stepForward(0, 0, 1) + } + + for (lesson in lessons) { + val date = Date.fromYmd(lesson[2]) + val startTime = Time.fromYmdHm(lesson[3]) + val endTime = Time.fromYmdHm(lesson[4]) + + dataDays.remove(date.value) + + val subjectId = data.subjectList.singleOrNull { it.longName == lesson[5] }?.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] + + Lesson(data.profileId, -1).also { + when (lesson[1]) { + "plan_lekcji", "lekcja" -> { + it.type = Lesson.TYPE_NORMAL + it.date = date + it.startTime = startTime + it.endTime = endTime + it.subjectId = subjectId + it.teacherId = teacherId + it.teamId = teamId + it.classroom = classroom + } + "lekcja_odwolana" -> { + it.type = Lesson.TYPE_CANCELLED + it.date = date + it.startTime = startTime + it.endTime = endTime + it.oldSubjectId = subjectId + //it.oldTeacherId = teacherId + it.oldTeamId = teamId + //it.oldClassroom = classroom + } + "zastepstwo" -> { + it.type = Lesson.TYPE_CHANGE + it.date = date + it.startTime = startTime + it.endTime = endTime + it.subjectId = subjectId + it.teacherId = teacherId + it.teamId = teamId + it.classroom = classroom + } + } + + it.id = it.buildId() + + val seen = profile.empty || date < Date.getToday() + + if (it.type != Lesson.TYPE_NORMAL) { + data.metadataList.add( + Metadata( + data.profileId, + Metadata.TYPE_LESSON_CHANGE, + it.id, + seen, + seen, + System.currentTimeMillis() + )) + } + data.lessonNewList += it + } + } + + for (day in dataDays) { + val lessonDate = Date.fromValue(day) + data.lessonNewList += Lesson(data.profileId, lessonDate.value.toLong()).apply { + type = Lesson.TYPE_NO_LESSONS + date = lessonDate + } + } + + /*for (lessonStr in rows) { + if (lessonStr.isNotEmpty()) { + val lesson = lessonStr.split("|") + + if (lesson[0].toInt() != data.studentId) + continue + + if (lesson[1] == "plan_lekcji" || lesson[1] == "lekcja") { + val lessonObject = Lesson(data.profileId, lesson[2], lesson[3], lesson[4]) + + data.subjectList.singleOrNull { it.longName == lesson[5] }?.let { + lessonObject.subjectId = it.id + } + data.teacherList.singleOrNull { it.fullNameLastFirst == (lesson[7]+" "+lesson[6]).fixName() }?.let { + lessonObject.teacherId = it.id + } + data.teamList.singleOrNull { it.name == lesson[8]+lesson[9] }?.let { + lessonObject.teamId = it.id + } + lessonObject.classroomName = lesson[11] + data.lessonList.add(lessonObject) + } + } + } + + // searching for all changes + for (lessonStr in rows) { + if (lessonStr.isNotEmpty()) { + val lesson = lessonStr.split("|") + + if (lesson[0].toInt() != data.studentId) + continue + + if (lesson[1] == "zastepstwo" || lesson[1] == "lekcja_odwolana") { + val lessonChange = LessonChange(data.profileId, lesson[2], lesson[3], lesson[4]) + + data.subjectList.singleOrNull { it.longName == lesson[5] }?.let { + lessonChange.subjectId = it.id + } + data.teacherList.singleOrNull { it.fullNameLastFirst == (lesson[7]+" "+lesson[6]).fixName() }?.let { + lessonChange.teacherId = it.id + } + data.teamList.singleOrNull { it.name == lesson[8]+lesson[9] }?.let { + lessonChange.teamId = it.id + } + + if (lesson[1] == "zastepstwo") { + lessonChange.type = LessonChange.TYPE_CHANGE + } + else if (lesson[1] == "lekcja_odwolana") { + lessonChange.type = LessonChange.TYPE_CANCELLED + } + else if (lesson[1] == "lekcja") { + lessonChange.type = LessonChange.TYPE_ADDED + } + lessonChange.classroomName = lesson[11] + + val originalLesson = lessonChange.getOriginalLesson(data.lessonList) + + if (lessonChange.type == LessonChange.TYPE_ADDED) { + if (originalLesson == null) { + // original lesson doesn't exist, save a new addition + // TODO + *//*if (!RegisterLessonChange.existsAddition(app.profile, registerLessonChange)) { + app.profile.timetable.addLessonAddition(registerLessonChange); + }*//* + } else { + // original lesson exists, so we need to compare them + if (!lessonChange.matches(originalLesson)) { + // the lessons are different, so it's probably a lesson change + // ahhh this damn API + lessonChange.type = LessonChange.TYPE_CHANGE + } + } + + } + if (lessonChange.type != LessonChange.TYPE_ADDED) { + // it's not a lesson addition + data.lessonChangeList.add(lessonChange) + data.metadataList.add( + Metadata( + data.profileId, + Metadata.TYPE_LESSON_CHANGE, + lessonChange.id, + data.profile?.empty ?: false, + data.profile?.empty ?: false, + System.currentTimeMillis() + )) + if (originalLesson == null) { + // there is no original lesson, so we have to add one in order to change it + data.lessonList.add(Lesson.fromLessonChange(lessonChange)) + } + } + } + } + }*/ + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiUsers.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiUsers.kt new file mode 100644 index 00000000..da17657d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiUsers.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-6. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api + +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.fixName + +class MobidziennikApiUsers(val data: DataMobidziennik, rows: List) { + init { + for (row in rows) { + if (row.isEmpty()) + continue + val cols = row.split("|") + + if (cols[1] != "*") + continue + + val id = cols[0].toLong() + val name = cols[4].fixName() + val surname = cols[5].fixName() + + data.teachersMap.put(id, "$surname $name") + data.teacherList.put(id, Teacher(data.profileId, id, name, surname)) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikLuckyNumberExtractor.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikLuckyNumberExtractor.kt new file mode 100644 index 00000000..7aaf7474 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikLuckyNumberExtractor.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-10. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.db.modules.luckynumber.LuckyNumber +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date + +class MobidziennikLuckyNumberExtractor(val data: DataMobidziennik, text: String) { + init { + data.profile?.luckyNumber = -1 + data.profile?.luckyNumberDate = null + + Regexes.MOBIDZIENNIK_LUCKY_NUMBER.find(text)?.let { + try { + val luckyNumber = it.groupValues[1].toInt() + + val luckyNumberObject = LuckyNumber( + data.profileId, + Date.getToday(), + luckyNumber + ) + + data.luckyNumberList.add(luckyNumberObject) + data.metadataList.add( + Metadata( + data.profileId, + Metadata.TYPE_LUCKY_NUMBER, + luckyNumberObject.date.value.toLong(), + data.profile?.empty ?: false, + data.profile?.empty ?: false, + System.currentTimeMillis() + )) + } catch (_: Exception){} + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebCalendar.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebCalendar.kt new file mode 100644 index 00000000..c1578af4 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebCalendar.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-10. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import com.google.gson.JsonParser +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_CALENDAR +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.utils.Utils.crc16 +import pl.szczodrzynski.edziennik.utils.models.Date +import java.util.* + +class MobidziennikWebCalendar(override val data: DataMobidziennik, + val onSuccess: () -> Unit) : MobidziennikWeb(data) { + companion object { + private const val TAG = "MobidziennikWebCalendar" + } + + init { + webGet(TAG, "/dziennik/kalendarzklasowy") { text -> + MobidziennikLuckyNumberExtractor(data, text) + + Regexes.MOBIDZIENNIK_CLASS_CALENDAR.find(text)?.let { + val events = JsonParser().parse(it.groupValues[1]).asJsonArray + for (eventEl in events) { + val event = eventEl.asJsonObject + + val idStr = event.getString("id") + if (idStr?.startsWith("kalendarz;") != true) { + continue + } + val idParts = idStr.split(";") + if (idParts.size < 2) { + continue + } + var id = idParts[2].toLongOrNull() ?: -1 + val studentId = idParts[1].toIntOrNull() ?: continue + if (studentId != data.studentId) + continue + + val dateString = event.getString("start") ?: continue + val eventDate = Date.fromY_m_d(dateString) + + val eventType = when (event.getString("color")?.toLowerCase(Locale.getDefault())) { + "#c54449" -> Event.TYPE_SHORT_QUIZ + "#ab0001" -> Event.TYPE_EXAM + "#008928" -> Event.TYPE_CLASS_EVENT + "#b66000" -> Event.TYPE_EXCURSION + else -> Event.TYPE_INFORMATION + } + + val title = event.getString("title") + val comment = event.getString("comment") + + var topic = title + if (title != comment) { + topic += "\n" + comment + } + + if (id == -1L) { + id = crc16(topic?.toByteArray()).toLong() + } + + val eventObject = Event( + profileId, + id, + eventDate, null, + topic, + -1, + eventType, + false, + -1, + -1, + data.teamClass?.id ?: -1 + ) + + data.eventList.add(eventObject) + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_EVENT, + eventObject.id, + profile?.empty ?: false, + profile?.empty ?: false, + System.currentTimeMillis() /* no addedDate here though */ + )) + } + } + + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_CALENDAR, SYNC_ALWAYS) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt new file mode 100644 index 00000000..326553c8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-28. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.utils.Utils +import java.io.File + +class MobidziennikWebGetAttachment( + override val data: DataMobidziennik, val message: Message, val attachmentId: Long, + val attachmentName: String, val onSuccess: () -> Unit) : MobidziennikWeb(data) { + companion object { + private const val TAG = "MobidziennikWebGetAttachment" + } + + init { + val targetFile = File(Utils.getStorageDir(), attachmentName) + + val typeUrl = if (message.type == Message.TYPE_SENT) + "wiadwyslana" + else + "wiadodebrana" + + webGetFile(TAG, "/dziennik/$typeUrl/?id=${message.id}&zalacznik=$attachmentId", targetFile, { file -> + + val event = AttachmentGetEvent( + profileId, + message.id, + attachmentId, + AttachmentGetEvent.TYPE_FINISHED, + file.absolutePath + ) + + val attachmentDataFile = File(Utils.getStorageDir(), ".${profileId}_${event.messageId}_${event.attachmentId}") + Utils.writeStringToFile(attachmentDataFile, event.fileName) + + EventBus.getDefault().post(event) + + onSuccess() + + }) { written, _ -> + // TODO make use of bytesTotal + val event = AttachmentGetEvent( + profileId, + message.id, + attachmentId, + AttachmentGetEvent.TYPE_PROGRESS, + bytesWritten = written + ) + + EventBus.getDefault().post(event) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt new file mode 100644 index 00000000..41e22e85 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt @@ -0,0 +1,158 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-18. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import org.greenrobot.eventbus.EventBus +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipientFull +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.fixName +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.utils.Utils.monthFromName +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +class MobidziennikWebGetMessage( + override val data: DataMobidziennik, + private val message: MessageFull, + val onSuccess: () -> Unit) : MobidziennikWeb(data) { + companion object { + private const val TAG = "MobidziennikWebGetMessage" + } + + init { + val typeUrl = if (message.type == Message.TYPE_SENT) + "wiadwyslana" + else + "wiadodebrana" + webGet(TAG, "/dziennik/$typeUrl/?id=${message.id}") { text -> + MobidziennikLuckyNumberExtractor(data, text) + + val messageRecipientList = mutableListOf() + + val doc = Jsoup.parse(text) + + val content = doc.select("#content").first() + + val body = content.select(".wiadomosc_tresc").first() + + if (message.type == TYPE_RECEIVED) { + var readDate = System.currentTimeMillis() + Regexes.MOBIDZIENNIK_MESSAGE_READ_DATE.find(body.html())?.let { + val date = Date( + it[3].toIntOrNull() ?: 2019, + monthFromName(it[2]), + it[1].toIntOrNull() ?: 1 + ) + val time = Time.fromH_m_s( + it[4] // TODO blank string safety + ) + readDate = date.combineWith(time) + } + + val recipient = MessageRecipientFull( + profileId, + -1, + -1, + readDate, + message.id + ) + + recipient.fullName = profile?.accountNameLong ?: profile?.studentNameLong ?: "" + + messageRecipientList.add(recipient) + } else { + message.senderId = -1 + message.senderReplyId = -1 + + content.select("table.spis tr:has(td)")?.forEach { recipientEl -> + val senderEl = recipientEl.select("td:eq(0)").first() + val senderName = senderEl.text().fixName() + + val teacher = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName } + val receiverId = teacher?.id ?: -1 + + var readDate = 0L + val isReadEl = recipientEl.select("td:eq(2)").first() + if (isReadEl.ownText() != "NIE") { + val readDateEl = recipientEl.select("td:eq(3) small").first() + Regexes.MOBIDZIENNIK_MESSAGE_SENT_READ_DATE.find(readDateEl.ownText())?.let { + val date = Date( + it[3].toIntOrNull() ?: 2019, + monthFromName(it[2]), + it[1].toIntOrNull() ?: 1 + ) + val time = Time.fromH_m_s( + it[4] // TODO blank string safety + ) + readDate = date.combineWith(time) + } + } + + val recipient = MessageRecipientFull( + profileId, + receiverId, + -1, + readDate, + message.id + ) + + recipient.fullName = teacher?.fullName ?: "?" + + messageRecipientList.add(recipient) + } + } + + // this line removes the sender and read date details + body.select("div").remove() + + // this needs to be at the end + message.apply { + this.body = body.html().replace("\n", "
    ") + + clearAttachments() + content.select("ul li").map { it.select("a").first() }.forEach { + val attachmentName = it.ownText() + Regexes.MOBIDZIENNIK_MESSAGE_ATTACHMENT.find(it.outerHtml())?.let { match -> + val attachmentId = match[1].toLong() + var size = match[2].toFloatOrNull() ?: -1f + when (match[3]) { + "K" -> size *= 1024f + "M" -> size *= 1024f * 1024f + "G" -> size *= 1024f * 1024f * 1024f + } + message.addAttachment(attachmentId, attachmentName, size.toLong()) + } + } + } + + if (!message.seen) { // TODO discover why this monstrosity instead of MetadataDao.setSeen + data.setSeenMetadataList.add(Metadata( + message.profileId, + Metadata.TYPE_MESSAGE, + message.id, + true, + true, + message.addedDate + )) + } + + message.recipients = messageRecipientList + data.messageRecipientList.addAll(messageRecipientList) + data.messageList.add(message) + + EventBus.getDefault().postSticky(MessageGetEvent(message)) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetRecipientList.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetRecipientList.kt new file mode 100644 index 00000000..bef95157 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetRecipientList.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-22. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import androidx.core.util.set +import androidx.room.OnConflictStrategy +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher + +class MobidziennikWebGetRecipientList( + override val data: DataMobidziennik, val onSuccess: () -> Unit) : MobidziennikWeb(data) { + companion object { + private const val TAG = "MobidziennikWebGetRecipientList" + } + + init { + webGet(TAG, "/mobile/dodajwiadomosc") { text -> + Regexes.MOBIDZIENNIK_MESSAGE_RECIPIENTS_JSON.find(text)?.let { match -> + val recipientLists = JsonParser().parse(match[1]).asJsonArray + recipientLists?.asJsonObjectList()?.forEach { list -> + val listType = list.getString("typ")?.toIntOrNull() ?: -1 + val listName = list.getString("nazwa") ?: "" + list.getJsonArray("dane")?.asJsonObjectList()?.forEach { recipient -> + if (recipient.getBoolean("lista") == true) { + recipient.getJsonArray("dane")?.asJsonObjectList()?.forEach { + processRecipient(listType, recipient.getString("nazwa") ?: "", it) + } + } + else + processRecipient(listType, listName, recipient) + } + } + } + + val event = RecipientListGetEvent( + data.profileId, + data.teacherList.filter { it.loginId != null } + ) + + profile?.lastReceiversSync = System.currentTimeMillis() + + data.teacherOnConflictStrategy = OnConflictStrategy.REPLACE + EventBus.getDefault().postSticky(event) + onSuccess() + } + } + + private fun processRecipient(listType: Int, listName: String, recipient: JsonObject) { + val id = recipient.getLong("id") ?: -1 + // get teacher by ID or create it + val teacher = data.teacherList[id] ?: Teacher(data.profileId, id).apply { + val fullName = recipient.getString("nazwa")?.fixName() + name = fullName ?: "" + fullName?.splitName()?.let { + name = it.second + surname = it.first + } + data.teacherList[id] = this + } + + teacher.apply { + loginId = id.toString() + when (listType) { + 1 -> setTeacherType(Teacher.TYPE_PRINCIPAL) + 2 -> setTeacherType(Teacher.TYPE_TEACHER) + 3 -> setTeacherType(Teacher.TYPE_PARENT) + 4 -> setTeacherType(Teacher.TYPE_STUDENT) + //5 -> Użytkownicy zewnętrzni + //6 -> Samorządy klasowe + 7 -> setTeacherType(Teacher.TYPE_PARENTS_COUNCIL) // Rady oddziałowe rodziców + 8 -> { + setTeacherType(Teacher.TYPE_EDUCATOR) + typeDescription = listName + } + 9 -> setTeacherType(Teacher.TYPE_PEDAGOGUE) + 10 -> setTeacherType(Teacher.TYPE_SPECIALIST) + else -> when (listName) { + "Administratorzy" -> setTeacherType(Teacher.TYPE_SCHOOL_ADMIN) + "Sekretarka" -> setTeacherType(Teacher.TYPE_SECRETARIAT) + "Wsparcie techniczne mobiDziennik" -> setTeacherType(Teacher.TYPE_SUPER_ADMIN) + else -> { + setTeacherType(Teacher.TYPE_OTHER) + typeDescription = listName + } + } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGrades.kt new file mode 100644 index 00000000..ccb0edfe --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGrades.kt @@ -0,0 +1,154 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-10. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import android.graphics.Color +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.fixWhiteSpaces +import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +class MobidziennikWebGrades(override val data: DataMobidziennik, + val onSuccess: () -> Unit) : MobidziennikWeb(data) { + companion object { + private const val TAG = "MobidziennikWebGrades" + } + + init { data.profile?.also { profile -> + val currentSemester = profile.currentSemester + + webGet(TAG, "/dziennik/oceny?semestr=$currentSemester") { text -> + MobidziennikLuckyNumberExtractor(data, text) + + val doc = Jsoup.parse(text) + + val grades = doc.select("table.spis a, table.spis span, table.spis div") + + var gradeCategory = "" + var gradeColor = -1 + var subjectName = "" + + for (e in grades) { + when (e.tagName()) { + "div" -> { + Regexes.MOBIDZIENNIK_GRADES_SUBJECT_NAME.find(e.outerHtml())?.let { + subjectName = it[1] + } + } + "span" -> { + val css = e.attr("style") + Regexes.MOBIDZIENNIK_GRADES_COLOR.find(css)?.let { + // (#2196f3) + gradeColor = Color.parseColor(it[1]) + } + Regexes.MOBIDZIENNIK_GRADES_CATEGORY.find(e.outerHtml())?.let { + // (category) + gradeCategory = it[1] + } + } + "a" -> { + val gradeId = e.attr("rel").toLong() + var gradeAddedDateMillis: Long = -1 + var gradeSemester = 1 + + val html = e.html() + val gradeClassAverage = Regexes.MOBIDZIENNIK_GRADES_CLASS_AVERAGE.find(html)?.let { + // (4.75) + it[1].toFloatOrNull() + } ?: -1f + + Regexes.MOBIDZIENNIK_GRADES_ADDED_DATE.find(html)?.let { + // (2) (stycznia) (2019), (12:34:56) + val month = when (it[2]) { + "stycznia" -> 1 + "lutego" -> 2 + "marca" -> 3 + "kwietnia" -> 4 + "maja" -> 5 + "czerwca" -> 6 + "lipca" -> 7 + "sierpnia" -> 8 + "września" -> 9 + "października" -> 10 + "listopada" -> 11 + "grudnia" -> 12 + else -> 1 + } + val gradeAddedDate = Date( + it[3].toInt(), + month, + it[1].toInt() + ) + val time = Time.fromH_m_s(it[4]) + gradeAddedDateMillis = gradeAddedDate.combineWith(time) + gradeSemester = profile.dateToSemester(gradeAddedDate) + } + + if (Regexes.MOBIDZIENNIK_GRADES_COUNT_TO_AVG.containsMatchIn(html)) { + Regexes.MOBIDZIENNIK_GRADES_DETAILS.find(html)?.let { match -> + val gradeName = match[1] + var gradeDescription = match[2] + val gradeValue = match[3].toFloatOrNull() ?: 0.0f + val teacherName = match[4].fixWhiteSpaces() + + val teacherId = data.teacherList.singleOrNull { it.fullNameLastFirst == teacherName }?.id ?: -1 + val subjectId = data.subjectList.singleOrNull { it.longName == subjectName }?.id ?: -1 + + if (match[5].isNotEmpty()) { + gradeDescription += "\n"+match[5].replace("
    ", "\n") + } + + val gradeObject = Grade( + profileId, + gradeId, + gradeCategory, + gradeColor, + "NLDŚR, $gradeDescription", + gradeName, + gradeValue, + 0f, + gradeSemester, + teacherId, + subjectId + ) + + gradeObject.classAverage = gradeClassAverage + + data.gradeList.add(gradeObject) + data.metadataList.add( + Metadata( + profileId, + Metadata.TYPE_GRADE, + gradeObject.id, + profile.empty, + profile.empty, + gradeAddedDateMillis + )) + } + } else { + data.gradeAverages.put(gradeId, gradeClassAverage) + data.gradeAddedDates.put(gradeId, gradeAddedDateMillis) + data.gradeColors.put(gradeId, gradeColor) + } + } + } + } + + data.toRemove.add(DataRemoveModel.Grades.semesterWithType(currentSemester, Grade.TYPE_NORMAL)) + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_GRADES, SYNC_ALWAYS) + onSuccess() + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt new file mode 100644 index 00000000..297f6eb3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.DAY +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.fixName +import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.utils.models.Date + +class MobidziennikWebMessagesAll(override val data: DataMobidziennik, + val onSuccess: () -> Unit) : MobidziennikWeb(data) { + companion object { + private const val TAG = "MobidziennikWebMessagesAll" + } + + init { + webGet(TAG, "/dziennik/wyszukiwarkawiadomosci?q=+") { text -> + MobidziennikLuckyNumberExtractor(data, text) + + val doc = Jsoup.parse(text) + + val listElement = doc.getElementsByClass("spis").first() + if (listElement == null) { + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL, 7*DAY) + onSuccess() + return@webGet + } + val list = listElement.getElementsByClass("podswietl") + for (item in list) { + val id = item.attr("rel").replace("[^\\d]".toRegex(), "").toLongOrNull() ?: continue + + val subjectEl = item.select("td:eq(0) div").first() + val subject = subjectEl.text() + + val addedDateEl = item.select("td:eq(1)").first() + val addedDate = Date.fromIsoHm(addedDateEl.text()) + + val typeEl = item.select("td:eq(2) img").first() + var type = TYPE_RECEIVED + if (typeEl.outerHtml().contains("mail_send.png")) + type = TYPE_SENT + + val senderEl = item.select("td:eq(3) div").first() + var senderId: Long = -1 + + if (type == TYPE_RECEIVED) { + // search sender teacher + val senderName = senderEl.text().fixName() + senderId = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName }?.id ?: -1 + data.messageRecipientList.add(MessageRecipient(profileId, -1, id)) + } else { + // TYPE_SENT, so multiple recipients possible + val recipientNames = senderEl.text().split(", ") + for (recipientName in recipientNames) { + val name = recipientName.fixName() + val recipientId = data.teacherList.singleOrNull { it.fullNameLastFirst == name }?.id ?: -1 + data.messageRecipientIgnoreList.add(MessageRecipient(profileId, recipientId, id)) + } + } + + val message = Message( + profileId, + id, + subject, + null, + type, + senderId, + -1 + ) + + data.messageIgnoreList.add(message) + data.metadataList.add(Metadata(profileId, Metadata.TYPE_MESSAGE, message.id, true, true, addedDate)) + } + + // sync every 7 days as we probably don't except more than + // 30 received messages during a week, without any normal sync + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL, 7*DAY) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesInbox.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesInbox.kt new file mode 100644 index 00000000..ef579b53 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesInbox.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_INBOX +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.fixName +import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.utils.models.Date + +class MobidziennikWebMessagesInbox(override val data: DataMobidziennik, + val onSuccess: () -> Unit) : MobidziennikWeb(data) { + companion object { + private const val TAG = "MobidziennikWebMessagesInbox" + } + + init { + webGet(TAG, "/dziennik/wiadomosci") { text -> + MobidziennikLuckyNumberExtractor(data, text) + + if (text.contains("Brak wiadomości odebranych.")) { + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_INBOX, SYNC_ALWAYS) + onSuccess() + return@webGet + } + + val doc = Jsoup.parse(text) + + val list = doc.getElementsByClass("spis").first().getElementsByClass("podswietl") + for (item in list) { + val id = item.attr("rel").toLongOrNull() ?: continue + + val subjectEl = item.select("td:eq(0)").first() + var hasAttachments = false + if (subjectEl.getElementsByTag("a").size != 0) { + hasAttachments = true + } + val subject = subjectEl.ownText() + + val addedDateEl = item.select("td:eq(1) small").first() + val addedDate = Date.fromIsoHm(addedDateEl.text()) + + val senderEl = item.select("td:eq(2)").first() + val senderName = senderEl.ownText().fixName() + val senderId = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName }?.id ?: -1 + data.messageRecipientIgnoreList.add(MessageRecipient(profileId, -1, id)) + + val isRead = item.select("td:eq(3) span").first().hasClass("wiadomosc_przeczytana") + + val message = Message( + profileId, + id, + subject, + null, + Message.TYPE_RECEIVED, + senderId, + -1 + ) + + if (hasAttachments) + message.setHasAttachments() + + data.messageIgnoreList.add(message) + data.setSeenMetadataList.add( + Metadata( + profileId, + Metadata.TYPE_MESSAGE, + message.id, + isRead, + isRead || profile?.empty ?: false, + addedDate + )) + } + + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_INBOX, SYNC_ALWAYS) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebNotices.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebNotices.kt new file mode 100644 index 00000000..d962826c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebNotices.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_NOTICES +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS + +class MobidziennikWebNotices(override val data: DataMobidziennik, + val onSuccess: () -> Unit) : MobidziennikWeb(data) { + companion object { + private const val TAG = "MobidziennikWebNotices" + } + + init { + // TODO this does no longer work: Mobidziennik changed their mobile page in 2019.09 + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_NOTICES, SYNC_ALWAYS) + onSuccess() + /*webGet(TAG, "/mobile/zachowanie") { text -> + MobidziennikLuckyNumberExtractor(data, text) + + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_NOTICES, SYNC_ALWAYS) + onSuccess() + }*/ + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt new file mode 100644 index 00000000..338d0de8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-26. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.POST +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher + +class MobidziennikWebSendMessage( + override val data: DataMobidziennik, + val recipients: List, + val subject: String, + val text: String, + val onSuccess: () -> Unit +) : MobidziennikWeb(data) { + companion object { + private const val TAG = "MobidziennikWebSendMessage" + } + + init { + val params = mutableListOf>( + "nazwa" to subject, + "tresc" to text + ) + for (teacher in recipients) { + teacher.loginId?.let { + params += "odbiorcy[]" to it + } + } + + webGet(TAG, endpoint = "/dziennik/dodajwiadomosc", method = POST, parameters = params) { text -> + + if (!text.contains(">Wiadomość została wysłana.<")) { + // TODO error + return@webGet + } + + // TODO create MobidziennikWebMessagesSent and replace this + MobidziennikWebMessagesAll(data) { + val message = data.messageIgnoreList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject } + val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id } + val event = MessageSentEvent(data.profileId, message, metadata?.addedDate) + + EventBus.getDefault().postSticky(event) + onSuccess() + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/firstlogin/MobidziennikFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/firstlogin/MobidziennikFirstLogin.kt new file mode 100644 index 00000000..e69aba2f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/firstlogin/MobidziennikFirstLogin.kt @@ -0,0 +1,59 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.firstlogin + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLoginWeb +import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.fixName +import pl.szczodrzynski.edziennik.utils.Utils + +class MobidziennikFirstLogin(val data: DataMobidziennik, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "MobidziennikFirstLogin" + } + + private val web = MobidziennikWeb(data) + private val profileList = mutableListOf() + + init { + MobidziennikLoginWeb(data) { + web.webGet(TAG, "/api/zrzutbazy") { text -> + val tables = text.split("T@B#LA") + + val accountNameLong = run { + tables[0] + .split("\n") + .map { it.split("|") } + .singleOrNull { it.getOrNull(1) != "*" } + ?.let { + "${it[4]} ${it[5]}".fixName() + } + } + + tables[8].split("\n").forEach { student -> + if (student.isEmpty()) + return@forEach + val student1 = student.split("|") + if (student1.size == 2) + return@forEach + + val profile = Profile() + profile.studentNameLong = "${student1[2]} ${student1[4]}".fixName() + profile.studentNameShort = "${student1[2]} ${student1[4][0]}.".fixName() + profile.accountNameLong = if (accountNameLong == profile.studentNameLong) null else accountNameLong + profile.studentSchoolYear = Utils.getCurrentSchoolYear() + profile.name = profile.studentNameLong + profile.subname = data.loginUsername + profile.empty = true + profile.putStudentData("studentId", student1[0].toInt()) + profileList.add(profile) + } + + EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore)) + onSuccess() + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLogin.kt new file mode 100644 index 00000000..498ab2e3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLogin.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_MOBIDZIENNIK_API2 +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_MOBIDZIENNIK_WEB +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.utils.Utils + +class MobidziennikLogin(val data: DataMobidziennik, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "MobidziennikLogin" + } + + private var cancelled = false + + init { + nextLoginMethod(onSuccess) + } + + private fun nextLoginMethod(onSuccess: () -> Unit) { + if (data.targetLoginMethodIds.isEmpty()) { + onSuccess() + return + } + if (cancelled) { + onSuccess() + return + } + useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId -> + data.progress(data.progressStep) + if (usedMethodId != -1) + data.loginMethods.add(usedMethodId) + nextLoginMethod(onSuccess) + } + } + + private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) { + // this should never be true + if (data.loginMethods.contains(loginMethodId)) { + onSuccess(-1) + return + } + Utils.d(TAG, "Using login method $loginMethodId") + when (loginMethodId) { + LOGIN_METHOD_MOBIDZIENNIK_WEB -> { + data.startProgress(R.string.edziennik_progress_login_mobidziennik_web) + MobidziennikLoginWeb(data) { onSuccess(loginMethodId) } + } + LOGIN_METHOD_MOBIDZIENNIK_API2 -> { + data.startProgress(R.string.edziennik_progress_login_mobidziennik_api2) + //MobidziennikLoginApi2(data) { onSuccess(loginMethodId) } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLoginWeb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLoginWeb.kt new file mode 100644 index 00000000..dbd3f86f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLoginWeb.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login + +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.callback.TextCallbackHandler +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.Mobidziennik +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.getUnixDate +import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.utils.Utils.d + +class MobidziennikLoginWeb(val data: DataMobidziennik, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "MobidziennikLoginWeb" + } + + init { run { + if (data.isWebLoginValid()) { + onSuccess() + } + else { + if (data.loginServerName.isNotNullNorEmpty() && data.loginUsername.isNotNullNorEmpty() && data.loginPassword.isNotNullNorEmpty()) { + data.app.cookieJar.clearForDomain(data.loginServerName + ".mobidziennik.pl") + loginWithCredentials() + } + else { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + } + } + }} + + private fun loginWithCredentials() { + d(TAG, "Request: Mobidziennik/Login/Web - https://${data.loginServerName}.mobidziennik.pl/api/") + + val callback = object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response?) { + if (text != "ok") { + when { + text == "Nie jestes zalogowany" -> ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN + text == "ifun" -> ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_DEVICE + text == "stare haslo" -> ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD + text == "Archiwum" -> ERROR_LOGIN_MOBIDZIENNIK_WEB_ARCHIVED + text == "Trwają prace techniczne lub pojawił się jakiś problem" -> ERROR_LOGIN_MOBIDZIENNIK_WEB_MAINTENANCE + text?.contains("Uuuups... nieprawidłowy adres") == true -> ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_ADDRESS + text?.contains("przerwa techniczna") == true -> ERROR_LOGIN_MOBIDZIENNIK_WEB_MAINTENANCE + else -> ERROR_LOGIN_MOBIDZIENNIK_WEB_OTHER + }.let { errorCode -> + data.error(ApiError(TAG, errorCode) + .withApiResponse(text) + .withResponse(response)) + return + } + } + + val cookies = data.app.cookieJar.getForDomain("${data.loginServerName}.mobidziennik.pl") + val cookie = cookies.singleOrNull { it.name().length > 32 } + val sessionKey = cookie?.name() + val sessionId = cookie?.value() + if (sessionId == null) { + data.error(ApiError(TAG, ERROR_LOGIN_MOBIDZIENNIK_WEB_NO_SESSION_ID) + .withResponse(response) + .withApiResponse(text)) + return + } + + data.webSessionKey = sessionKey + data.webSessionValue = sessionId + data.webServerId = data.app.cookieJar.getCookie("${data.loginServerName}.mobidziennik.pl", "SERVERID") + data.webSessionIdExpiryTime = response.getUnixDate() + 45 * 60 /* 45min */ + onSuccess() + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + + Request.builder() + .url("https://${data.loginServerName}.mobidziennik.pl/api/") + .userAgent(MOBIDZIENNIK_USER_AGENT) + .contentType("application/x-www-form-urlencoded; charset=UTF-8") + .addParameter("wersja", "20") + .addParameter("ip", data.app.deviceId) + .addParameter("login", data.loginUsername) + .addParameter("haslo", data.loginPassword) + .addParameter("token", data.app.config.sync.tokenMobidziennik) + .addParameter("ta_api", Mobidziennik.API_KEY) + .post() + .callback(callback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/DataTemplate.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/DataTemplate.kt new file mode 100644 index 00000000..86616983 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/DataTemplate.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.template + +import okhttp3.Cookie +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_TEMPLATE_API +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_TEMPLATE_WEB +import pl.szczodrzynski.edziennik.data.api.models.Data +import pl.szczodrzynski.edziennik.currentTimeUnix +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.isNotNullNorEmpty + +/** + * Use http://patorjk.com/software/taag/#p=display&f=Big for the ascii art + * + * Use https://codepen.io/kubasz/pen/RwwwbGN to easily generate the student data getters/setters + */ +class DataTemplate(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { + + fun isWebLoginValid() = webExpiryTime-30 > currentTimeUnix() && webCookie.isNotNullNorEmpty() + fun isApiLoginValid() = apiExpiryTime-30 > currentTimeUnix() && apiToken.isNotNullNorEmpty() + + override fun satisfyLoginMethods() { + loginMethods.clear() + if (isWebLoginValid()) { + loginMethods += LOGIN_METHOD_TEMPLATE_WEB + app.cookieJar.saveFromResponse(null, listOf( + Cookie.Builder() + .name("AuthCookie") + .value(webCookie!!) + .domain("eregister.example.com") + .secure().httpOnly().build() + )) + } + if (isApiLoginValid()) + loginMethods += LOGIN_METHOD_TEMPLATE_API + } + + /* __ __ _ + \ \ / / | | + \ \ /\ / /__| |__ + \ \/ \/ / _ \ '_ \ + \ /\ / __/ |_) | + \/ \/ \___|_._*/ + private var mWebCookie: String? = null + var webCookie: String? + get() { mWebCookie = mWebCookie ?: profile?.getStudentData("webCookie", null); return mWebCookie } + set(value) { profile?.putStudentData("webCookie", value) ?: return; mWebCookie = value } + + private var mWebExpiryTime: Long? = null + var webExpiryTime: Long + get() { mWebExpiryTime = mWebExpiryTime ?: profile?.getStudentData("webExpiryTime", 0L); return mWebExpiryTime ?: 0L } + set(value) { profile?.putStudentData("webExpiryTime", value) ?: return; mWebExpiryTime = value } + + /* _ + /\ (_) + / \ _ __ _ + / /\ \ | '_ \| | + / ____ \| |_) | | + /_/ \_\ .__/|_| + | | + |*/ + private var mApiToken: String? = null + var apiToken: String? + get() { mApiToken = mApiToken ?: profile?.getStudentData("apiToken", null); return mApiToken } + set(value) { profile?.putStudentData("apiToken", value) ?: return; mApiToken = value } + + private var mApiExpiryTime: Long? = null + var apiExpiryTime: Long + get() { mApiExpiryTime = mApiExpiryTime ?: profile?.getStudentData("apiExpiryTime", 0L); return mApiExpiryTime ?: 0L } + set(value) { profile?.putStudentData("apiExpiryTime", value) ?: return; mApiExpiryTime = value } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt new file mode 100644 index 00000000..0bae4226 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.template + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.CODE_INTERNAL_LIBRUS_ACCOUNT_410 +import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.TemplateData +import pl.szczodrzynski.edziennik.data.api.edziennik.template.firstlogin.TemplateFirstLogin +import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLogin +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.prepare +import pl.szczodrzynski.edziennik.data.api.templateLoginMethods +import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.utils.Utils.d + +class Template(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface { + companion object { + private const val TAG = "Template" + } + + val internalErrorList = mutableListOf() + val data: DataTemplate + + init { + data = DataTemplate(app, profile, loginStore).apply { + callback = wrapCallback(this@Template.callback) + satisfyLoginMethods() + } + } + + private fun completed() { + data.saveData() + data.notify { + callback.onCompleted() + } + } + + /* _______ _ _ _ _ _ + |__ __| | /\ | | (_) | | | + | | | |__ ___ / \ | | __ _ ___ _ __ _| |_| |__ _ __ ___ + | | | '_ \ / _ \ / /\ \ | |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \ + | | | | | | __/ / ____ \| | (_| | (_) | | | | |_| | | | | | | | | + |_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| + __/ | + |__*/ + override fun sync(featureIds: List, viewId: Int?, arguments: JsonObject?) { + data.arguments = arguments + data.prepare(templateLoginMethods, TemplateFeatures, featureIds, viewId) + d(TAG, "LoginMethod IDs: ${data.targetLoginMethodIds}") + d(TAG, "Endpoint IDs: ${data.targetEndpointIds}") + TemplateLogin(data) { + TemplateData(data) { + completed() + } + } + } + + override fun getMessage(message: MessageFull) { + + } + + override fun sendMessage(recipients: List, subject: String, text: String) { + + } + + override fun markAllAnnouncementsAsRead() { + + } + + override fun getAnnouncement(announcement: AnnouncementFull) { + + } + + override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) { + + } + + override fun getRecipientList() { + + } + + override fun firstLogin() { + TemplateFirstLogin(data) { + completed() + } + } + + override fun cancel() { + d(TAG, "Cancelled") + data.cancel() + } + + private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { + return object : EdziennikCallback { + override fun onCompleted() { + callback.onCompleted() + } + + override fun onProgress(step: Float) { + callback.onProgress(step) + } + + override fun onStartProgress(stringRes: Int) { + callback.onStartProgress(stringRes) + } + + override fun onError(apiError: ApiError) { + when (apiError.errorCode) { + in internalErrorList -> { + // finish immediately if the same error occurs twice during the same sync + callback.onError(apiError) + } + CODE_INTERNAL_LIBRUS_ACCOUNT_410 -> { + internalErrorList.add(apiError.errorCode) + loginStore.removeLoginData("refreshToken") // force a clean login + //loginLibrus() + } + else -> callback.onError(apiError) + } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/TemplateFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/TemplateFeatures.kt new file mode 100644 index 00000000..c12e2bcc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/TemplateFeatures.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.template + +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.models.Feature + +const val ENDPOINT_TEMPLATE_WEB_SAMPLE = 9991 +const val ENDPOINT_TEMPLATE_WEB_SAMPLE_2 = 9992 +const val ENDPOINT_TEMPLATE_API_SAMPLE = 9993 + +val TemplateFeatures = listOf( + Feature(LOGIN_TYPE_TEMPLATE, FEATURE_STUDENT_INFO, listOf( + ENDPOINT_TEMPLATE_WEB_SAMPLE to LOGIN_METHOD_TEMPLATE_WEB + ), listOf(LOGIN_METHOD_TEMPLATE_WEB)), + Feature(LOGIN_TYPE_TEMPLATE, FEATURE_SCHOOL_INFO, listOf( + ENDPOINT_TEMPLATE_WEB_SAMPLE_2 to LOGIN_METHOD_TEMPLATE_WEB + ), listOf(LOGIN_METHOD_TEMPLATE_WEB)), + Feature(LOGIN_TYPE_TEMPLATE, FEATURE_GRADES, listOf( + ENDPOINT_TEMPLATE_API_SAMPLE to LOGIN_METHOD_TEMPLATE_API + ), listOf(LOGIN_METHOD_TEMPLATE_API)) +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateApi.kt new file mode 100644 index 00000000..e36a8e6d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateApi.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.template.data + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.data.api.ERROR_TEMPLATE_WEB_OTHER +import pl.szczodrzynski.edziennik.data.api.GET +import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.currentTimeUnix + +open class TemplateApi(open val data: DataTemplate) { + companion object { + private const val TAG = "TemplateApi" + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + /** + * This will be used by all TemplateApi* endpoints. + * + * You can customize this method's parameters to best fit the implemented e-register. + * Just make sure that [tag] and [onSuccess] is present. + */ + fun apiGet(tag: String, endpoint: String, method: Int = GET, payload: JsonObject? = null, onSuccess: (json: JsonObject?) -> Unit) { + val json = JsonObject() + json.addProperty("foo", "bar") + json.addProperty("sample", "text") + + if (currentTimeUnix() % 4L == 0L) { + // let's set a 20% chance of error, just as a test + data.error(ApiError(tag, ERROR_TEMPLATE_WEB_OTHER) + .withApiResponse("404 Not Found - this is the text returned by the API")) + return + } + + onSuccess(json) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateData.kt new file mode 100644 index 00000000..d0833c02 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateData.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.template.data + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate +import pl.szczodrzynski.edziennik.data.api.edziennik.template.ENDPOINT_TEMPLATE_API_SAMPLE +import pl.szczodrzynski.edziennik.data.api.edziennik.template.ENDPOINT_TEMPLATE_WEB_SAMPLE +import pl.szczodrzynski.edziennik.data.api.edziennik.template.ENDPOINT_TEMPLATE_WEB_SAMPLE_2 +import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.api.TemplateApiSample +import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample +import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample2 +import pl.szczodrzynski.edziennik.utils.Utils + +class TemplateData(val data: DataTemplate, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "TemplateData" + } + + init { + nextEndpoint(onSuccess) + } + + private fun nextEndpoint(onSuccess: () -> Unit) { + if (data.targetEndpointIds.isEmpty()) { + onSuccess() + return + } + if (data.cancelled) { + onSuccess() + return + } + useEndpoint(data.targetEndpointIds.removeAt(0)) { + data.progress(data.progressStep) + nextEndpoint(onSuccess) + } + } + + private fun useEndpoint(endpointId: Int, onSuccess: () -> Unit) { + Utils.d(TAG, "Using endpoint $endpointId") + when (endpointId) { + ENDPOINT_TEMPLATE_WEB_SAMPLE -> { + data.startProgress(R.string.edziennik_progress_endpoint_student_info) + TemplateWebSample(data) { onSuccess() } + } + ENDPOINT_TEMPLATE_WEB_SAMPLE_2 -> { + data.startProgress(R.string.edziennik_progress_endpoint_school_info) + TemplateWebSample2(data) { onSuccess() } + } + ENDPOINT_TEMPLATE_API_SAMPLE -> { + data.startProgress(R.string.edziennik_progress_endpoint_grades) + TemplateApiSample(data) { onSuccess() } + } + else -> onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateWeb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateWeb.kt new file mode 100644 index 00000000..eabe71a6 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateWeb.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.template.data + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.data.api.ERROR_TEMPLATE_WEB_OTHER +import pl.szczodrzynski.edziennik.data.api.GET +import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.currentTimeUnix + +open class TemplateWeb(open val data: DataTemplate) { + companion object { + private const val TAG = "TemplateWeb" + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + /** + * This will be used by all TemplateWeb* endpoints. + * + * You can customize this method's parameters to best fit the implemented e-register. + * Just make sure that [tag] and [onSuccess] is present. + */ + fun webGet(tag: String, endpoint: String, method: Int = GET, payload: JsonObject? = null, onSuccess: (json: JsonObject?) -> Unit) { + val json = JsonObject() + json.addProperty("foo", "bar") + json.addProperty("sample", "text") + + if (currentTimeUnix() % 4L == 0L) { + // let's set a 20% chance of error, just as a test + data.error(ApiError(tag, ERROR_TEMPLATE_WEB_OTHER) + .withApiResponse("404 Not Found - this is the text returned by the API")) + return + } + + onSuccess(json) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/api/TemplateApiSample.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/api/TemplateApiSample.kt new file mode 100644 index 00000000..e8f0a326 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/api/TemplateApiSample.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.template.data.api + +import pl.szczodrzynski.edziennik.DAY +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate +import pl.szczodrzynski.edziennik.data.api.edziennik.template.ENDPOINT_TEMPLATE_API_SAMPLE +import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.TemplateApi +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS + +class TemplateApiSample(override val data: DataTemplate, + val onSuccess: () -> Unit) : TemplateApi(data) { + companion object { + private const val TAG = "TemplateApiSample" + } + + init { + apiGet(TAG, "/api/v3/getData.php") { json -> + // here you can access and update any fields of the `data` object + + // ================ + // schedule a sync: + + // not sooner than two days later + data.setSyncNext(ENDPOINT_TEMPLATE_API_SAMPLE, 2 * DAY) + // in two days OR on explicit "grades" sync + data.setSyncNext(ENDPOINT_TEMPLATE_API_SAMPLE, 2 * DAY, MainActivity.DRAWER_ITEM_GRADES) + // only if sync is executed on Home view + data.setSyncNext(ENDPOINT_TEMPLATE_API_SAMPLE, syncIn = null, viewId = MainActivity.DRAWER_ITEM_HOME) + // always, in every sync + data.setSyncNext(ENDPOINT_TEMPLATE_API_SAMPLE, SYNC_ALWAYS) + + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/web/TemplateWebSample.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/web/TemplateWebSample.kt new file mode 100644 index 00000000..5efca695 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/web/TemplateWebSample.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web + +import pl.szczodrzynski.edziennik.DAY +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_GRADES +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOME +import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate +import pl.szczodrzynski.edziennik.data.api.edziennik.template.ENDPOINT_TEMPLATE_WEB_SAMPLE +import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.TemplateWeb +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS + +class TemplateWebSample(override val data: DataTemplate, + val onSuccess: () -> Unit) : TemplateWeb(data) { + companion object { + private const val TAG = "TemplateWebSample" + } + + init { + webGet(TAG, "/api/v3/getData.php") { json -> + // here you can access and update any fields of the `data` object + + // ================ + // schedule a sync: + + // not sooner than two days later + data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE, 2 * DAY) + // in two days OR on explicit "grades" sync + data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE, 2 * DAY, DRAWER_ITEM_GRADES) + // only if sync is executed on Home view + data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE, syncIn = null, viewId = DRAWER_ITEM_HOME) + // always, in every sync + data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE, SYNC_ALWAYS) + + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/web/TemplateWebSample2.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/web/TemplateWebSample2.kt new file mode 100644 index 00000000..af12c16e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/web/TemplateWebSample2.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web + +import pl.szczodrzynski.edziennik.DAY +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate +import pl.szczodrzynski.edziennik.data.api.edziennik.template.ENDPOINT_TEMPLATE_WEB_SAMPLE_2 +import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.TemplateWeb +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS + +class TemplateWebSample2(override val data: DataTemplate, + val onSuccess: () -> Unit) : TemplateWeb(data) { + companion object { + private const val TAG = "TemplateWebSample2" + } + + init { + webGet(TAG, "/api/v3/getData.php") { json -> + // here you can access and update any fields of the `data` object + + // ================ + // schedule a sync: + + // not sooner than two days later + data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE_2, 2 * DAY) + // in two days OR on explicit "grades" sync + data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE_2, 2 * DAY, MainActivity.DRAWER_ITEM_GRADES) + // only if sync is executed on Home view + data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE_2, syncIn = null, viewId = MainActivity.DRAWER_ITEM_HOME) + // always, in every sync + data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE_2, SYNC_ALWAYS) + + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/firstlogin/TemplateFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/firstlogin/TemplateFirstLogin.kt new file mode 100644 index 00000000..54654f08 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/firstlogin/TemplateFirstLogin.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-27. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.template.firstlogin + +import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate +import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.TemplateApi +import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.TemplateWeb +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile + +class TemplateFirstLogin(val data: DataTemplate, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "TemplateFirstLogin" + } + + private val web = TemplateWeb(data) + private val api = TemplateApi(data) + private val profileList = mutableListOf() + + init { + /*TemplateLoginWeb(data) { + web.webGet(TAG, "get all accounts") { text -> + //val accounts = json.getJsonArray("accounts") + + EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore)) + onSuccess() + } + }*/ + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLogin.kt new file mode 100644 index 00000000..b11eef13 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLogin.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.template.login + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_TEMPLATE_API +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_TEMPLATE_WEB +import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate +import pl.szczodrzynski.edziennik.utils.Utils + +class TemplateLogin(val data: DataTemplate, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "TemplateLogin" + } + + private var cancelled = false + + init { + nextLoginMethod(onSuccess) + } + + private fun nextLoginMethod(onSuccess: () -> Unit) { + if (data.targetLoginMethodIds.isEmpty()) { + onSuccess() + return + } + if (cancelled) { + onSuccess() + return + } + useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId -> + data.progress(data.progressStep) + if (usedMethodId != -1) + data.loginMethods.add(usedMethodId) + nextLoginMethod(onSuccess) + } + } + + private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) { + // this should never be true + if (data.loginMethods.contains(loginMethodId)) { + onSuccess(-1) + return + } + Utils.d(TAG, "Using login method $loginMethodId") + when (loginMethodId) { + LOGIN_METHOD_TEMPLATE_WEB -> { + data.startProgress(R.string.edziennik_progress_login_template_web) + TemplateLoginWeb(data) { onSuccess(loginMethodId) } + } + LOGIN_METHOD_TEMPLATE_API -> { + data.startProgress(R.string.edziennik_progress_login_template_api) + TemplateLoginApi(data) { onSuccess(loginMethodId) } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLoginApi.kt new file mode 100644 index 00000000..fc1fc20e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLoginApi.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.template.login + +import pl.szczodrzynski.edziennik.HOUR +import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_DATA_MISSING +import pl.szczodrzynski.edziennik.data.api.ERROR_PROFILE_MISSING +import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.currentTimeUnix + +class TemplateLoginApi(val data: DataTemplate, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "TemplateLoginApi" + } + + init { run { + if (data.profile == null) { + data.error(ApiError(TAG, ERROR_PROFILE_MISSING)) + return@run + } + + if (data.isApiLoginValid()) { + onSuccess() + } + else { + if (/*data.webLogin != null && data.webPassword != null && */true) { + loginWithCredentials() + } + else { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + } + } + }} + + fun loginWithCredentials() { + // succeed immediately + + data.apiToken = "ThisIsAVeryLongToken" + data.apiExpiryTime = currentTimeUnix() + 24 * HOUR + onSuccess() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLoginWeb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLoginWeb.kt new file mode 100644 index 00000000..fac39d50 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLoginWeb.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-5. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.template.login + +import okhttp3.Cookie +import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_DATA_MISSING +import pl.szczodrzynski.edziennik.data.api.ERROR_PROFILE_MISSING +import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.currentTimeUnix + +class TemplateLoginWeb(val data: DataTemplate, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "TemplateLoginWeb" + } + + init { run { + if (data.profile == null) { + data.error(ApiError(TAG, ERROR_PROFILE_MISSING)) + return@run + } + + if (data.isWebLoginValid()) { + data.app.cookieJar.saveFromResponse(null, listOf( + Cookie.Builder() + .name("AuthCookie") + .value(data.webCookie!!) + .domain("eregister.example.com") + .secure().httpOnly().build() + )) + onSuccess() + } + else { + data.app.cookieJar.clearForDomain("eregister.example.com") + if (/*data.webLogin != null && data.webPassword != null && */true) { + loginWithCredentials() + } + else { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + } + } + }} + + fun loginWithCredentials() { + // succeed immediately + + data.webCookie = "ThisIsACookie" + data.webExpiryTime = currentTimeUnix() + 45 * 60 /* 45min */ + onSuccess() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt new file mode 100644 index 00000000..8a7e609b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt @@ -0,0 +1,184 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-6. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan + +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API +import pl.szczodrzynski.edziennik.data.api.models.Data +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.isNotNullNorEmpty + +class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { + + fun isApiLoginValid() = /*apiCertificateExpiryTime-30 > currentTimeUnix() + &&*/ apiCertificateKey.isNotNullNorEmpty() + && apiCertificatePrivate.isNotNullNorEmpty() + && symbol.isNotNullNorEmpty() + + override fun satisfyLoginMethods() { + loginMethods.clear() + if (isApiLoginValid()) { + loginMethods += LOGIN_METHOD_VULCAN_API + } + } + + /** + * A UONET+ client symbol. + * + * Present in the URL: https://uonetplus-uczen.vulcan.net.pl/[symbol]/[schoolSymbol]/ + * + * e.g. "poznan" + */ + private var mSymbol: String? = null + var symbol: String? + get() { mSymbol = mSymbol ?: loginStore.getLoginData("deviceSymbol", null); return mSymbol } + set(value) { loginStore.putLoginData("deviceSymbol", value); mSymbol = value } + + /** + * Group symbol/number of the student's school. + * + * Present in the URL: https://uonetplus-uczen.vulcan.net.pl/[symbol]/[schoolSymbol]/ + * + * ListaUczniow/JednostkaSprawozdawczaSymbol, e.g. "000088" + */ + private var mSchoolSymbol: String? = null + var schoolSymbol: String? + get() { mSchoolSymbol = mSchoolSymbol ?: profile?.getStudentData("schoolSymbol", null); return mSchoolSymbol } + set(value) { profile?.putStudentData("schoolSymbol", value) ?: return; mSchoolSymbol = value } + + /** + * A school ID consisting of the [symbol] and [schoolSymbol]. + * + * [symbol]_[schoolSymbol] + * + * e.g. "poznan_000088" + */ + private var mSchoolName: String? = null + var schoolName: String? + get() { mSchoolName = mSchoolName ?: profile?.getStudentData("schoolName", null); return mSchoolName } + set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolName = value } + + /** + * ID of the student. + * + * ListaUczniow/Id, e.g. 42632 + */ + private var mStudentId: Int? = null + var studentId: Int + get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 } + set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } + + /** + * ID of the student's account. + * + * ListaUczniow/UzytkownikLoginId, e.g. 1709 + */ + private var mStudentLoginId: Int? = null + var studentLoginId: Int + get() { mStudentLoginId = mStudentLoginId ?: profile?.getStudentData("studentLoginId", 0); return mStudentLoginId ?: 0 } + set(value) { profile?.putStudentData("studentLoginId", value) ?: return; mStudentLoginId = value } + + /** + * ID of the student's class. + * + * ListaUczniow/IdOddzial, e.g. 35 + */ + private var mStudentClassId: Int? = null + var studentClassId: Int + get() { mStudentClassId = mStudentClassId ?: profile?.getStudentData("studentClassId", 0); return mStudentClassId ?: 0 } + set(value) { profile?.putStudentData("studentClassId", value) ?: return; mStudentClassId = value } + + /** + * ListaUczniow/IdOkresKlasyfikacyjny, e.g. 321 + */ + private var mStudentSemesterId: Int? = null + var studentSemesterId: Int + get() { mStudentSemesterId = mStudentSemesterId ?: profile?.getStudentData("studentSemesterId", 0); return mStudentSemesterId ?: 0 } + set(value) { profile?.putStudentData("studentSemesterId", value) ?: return; mStudentSemesterId = value } + + /** + * ListaUczniow/OkresNumer, e.g. 1 or 2 + */ + private var mStudentSemesterNumber: Int? = null + var studentSemesterNumber: Int + get() { mStudentSemesterNumber = mStudentSemesterNumber ?: profile?.getStudentData("studentSemesterNumber", 0); return mStudentSemesterNumber ?: 0 } + set(value) { profile?.putStudentData("studentSemesterNumber", value) ?: return; mStudentSemesterNumber = value } + + /* _____ _____ ____ + /\ | __ \_ _| |___ \ + / \ | |__) || | __ ____) | + / /\ \ | ___/ | | \ \ / /__ < + / ____ \| | _| |_ \ V /___) | + /_/ \_\_| |_____| \_/|___*/ + /** + * A mobile API registration token. + * + * After first login only 3 first characters are stored here. + * This is later used to determine the API URL address. + */ + private var mApiToken: String? = null + var apiToken: String? + get() { mApiToken = mApiToken ?: loginStore.getLoginData("deviceToken", null); return mApiToken } + set(value) { loginStore.putLoginData("deviceToken", value); mApiToken = value } + + /** + * A mobile API registration PIN. + * + * After first login, this is removed and/or set to null. + */ + private var mApiPin: String? = null + var apiPin: String? + get() { mApiPin = mApiPin ?: loginStore.getLoginData("devicePin", null); return mApiPin } + set(value) { loginStore.putLoginData("devicePin", value); mApiPin = value } + + private var mApiCertificateKey: String? = null + var apiCertificateKey: String? + get() { mApiCertificateKey = mApiCertificateKey ?: loginStore.getLoginData("certificateKey", null); return mApiCertificateKey } + set(value) { loginStore.putLoginData("certificateKey", value); mApiCertificateKey = value } + + private var mApiCertificatePfx: String? = null + var apiCertificatePfx: String? + get() { mApiCertificatePfx = mApiCertificatePfx ?: loginStore.getLoginData("certificatePfx", null); return mApiCertificatePfx } + set(value) { loginStore.putLoginData("certificatePfx", value); mApiCertificatePfx = value } + + private var mApiCertificatePrivate: String? = null + var apiCertificatePrivate: String? + get() { mApiCertificatePrivate = mApiCertificatePrivate ?: loginStore.getLoginData("certificatePrivate", null); return mApiCertificatePrivate } + set(value) { loginStore.putLoginData("certificatePrivate", value); mApiCertificatePrivate = value } + + private var mApiCertificateExpiryTime: Int? = null + var apiCertificateExpiryTime: Int + get() { mApiCertificateExpiryTime = mApiCertificateExpiryTime ?: loginStore.getLoginData("certificateExpiryTime", 0); return mApiCertificateExpiryTime ?: 0 } + set(value) { loginStore.putLoginData("certificateExpiryTime", value); mApiCertificateExpiryTime = value } + + val apiUrl: String? + get() { + val url = when (apiToken?.substring(0, 3)) { + "3S1" -> "https://lekcjaplus.vulcan.net.pl" + "TA1" -> "https://uonetplus-komunikacja.umt.tarnow.pl" + "OP1" -> "https://uonetplus-komunikacja.eszkola.opolskie.pl" + "RZ1" -> "https://uonetplus-komunikacja.resman.pl" + "GD1" -> "https://uonetplus-komunikacja.edu.gdansk.pl" + "KA1" -> "https://uonetplus-komunikacja.mcuw.katowice.eu" + "KA2" -> "https://uonetplus-komunikacja-test.mcuw.katowice.eu" + "LU1" -> "https://uonetplus-komunikacja.edu.lublin.eu" + "LU2" -> "https://test-uonetplus-komunikacja.edu.lublin.eu" + "P03" -> "https://efeb-komunikacja-pro-efebmobile.pro.vulcan.pl" + "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" + "FK1", "FS1" -> "http://api.fakelog.cf" + "SZ9" -> "http://hack.szkolny.eu" + else -> null + } + return if (url != null) "$url/$symbol" else loginStore.getLoginData("apiUrl", null) + } + + val fullApiUrl: String? + get() { + return "$apiUrl/$schoolSymbol" + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt new file mode 100644 index 00000000..f835f29b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt @@ -0,0 +1,146 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-6. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanData +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiMessagesChangeStatus +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiSendMessage +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.firstlogin.VulcanFirstLogin +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLogin +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.prepare +import pl.szczodrzynski.edziennik.data.api.prepareFor +import pl.szczodrzynski.edziennik.data.api.vulcanLoginMethods +import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.utils.Utils.d + +class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface { + companion object { + private const val TAG = "Vulcan" + } + + val internalErrorList = mutableListOf() + val data: DataVulcan + private var afterLogin: (() -> Unit)? = null + + init { + data = DataVulcan(app, profile, loginStore).apply { + callback = wrapCallback(this@Vulcan.callback) + satisfyLoginMethods() + } + } + + private fun completed() { + data.saveData() + data.notify { + callback.onCompleted() + } + } + + /* _______ _ _ _ _ _ + |__ __| | /\ | | (_) | | | + | | | |__ ___ / \ | | __ _ ___ _ __ _| |_| |__ _ __ ___ + | | | '_ \ / _ \ / /\ \ | |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \ + | | | | | | __/ / ____ \| | (_| | (_) | | | | |_| | | | | | | | | + |_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| + __/ | + |__*/ + override fun sync(featureIds: List, viewId: Int?, arguments: JsonObject?) { + data.arguments = arguments + data.prepare(vulcanLoginMethods, VulcanFeatures, featureIds, viewId) + login() + } + + private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) { + d(TAG, "Trying to login with ${data.targetLoginMethodIds}") + if (internalErrorList.isNotEmpty()) { + d(TAG, " - Internal errors:") + internalErrorList.forEach { d(TAG, " - code $it") } + } + loginMethodId?.let { data.prepareFor(vulcanLoginMethods, it) } + afterLogin?.let { this.afterLogin = it } + VulcanLogin(data) { + data() + } + } + + private fun data() { + d(TAG, "Endpoint IDs: ${data.targetEndpointIds}") + if (internalErrorList.isNotEmpty()) { + d(TAG, " - Internal errors:") + internalErrorList.forEach { d(TAG, " - code $it") } + } + afterLogin?.invoke() ?: VulcanData(data) { + completed() + } + } + + override fun getMessage(message: MessageFull) { + login(LOGIN_METHOD_VULCAN_API) { + VulcanApiMessagesChangeStatus(data, message) { + completed() + } + } + } + + override fun sendMessage(recipients: List, subject: String, text: String) { + login(LOGIN_METHOD_VULCAN_API) { + VulcanApiSendMessage(data, recipients, subject, text) { + completed() + } + } + } + + override fun markAllAnnouncementsAsRead() { + + } + + override fun getAnnouncement(announcement: AnnouncementFull) { + + } + + override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) { + + } + + override fun getRecipientList() { + + } + + override fun firstLogin() { VulcanFirstLogin(data) { completed() } } + override fun cancel() { + d(TAG, "Cancelled") + data.cancel() + } + + private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { + return object : EdziennikCallback { + override fun onCompleted() { callback.onCompleted() } + override fun onProgress(step: Float) { callback.onProgress(step) } + override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) } + override fun onError(apiError: ApiError) { + if (apiError.errorCode in internalErrorList) { + // finish immediately if the same error occurs twice during the same sync + callback.onError(apiError) + return + } + internalErrorList.add(apiError.errorCode) + when (apiError.errorCode) { + else -> callback.onError(apiError) + } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt new file mode 100644 index 00000000..bf6c080b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-6. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan + +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.models.Feature + +const val ENDPOINT_VULCAN_API_STUDENT_LIST = 1000 +const val ENDPOINT_VULCAN_API_DICTIONARIES = 1010 +const val ENDPOINT_VULCAN_API_TIMETABLE = 1020 +const val ENDPOINT_VULCAN_API_EVENTS = 1030 +const val ENDPOINT_VULCAN_API_GRADES = 1040 +const val ENDPOINT_VULCAN_API_GRADES_SUMMARY = 1050 +const val ENDPOINT_VULCAN_API_HOMEWORK = 1060 +const val ENDPOINT_VULCAN_API_NOTICES = 1070 +const val ENDPOINT_VULCAN_API_ATTENDANCE = 1080 +const val ENDPOINT_VULCAN_API_MESSAGES_INBOX = 1090 +const val ENDPOINT_VULCAN_API_MESSAGES_SENT = 1100 + +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)), + // agenda + Feature(LOGIN_TYPE_VULCAN, FEATURE_AGENDA, listOf( + ENDPOINT_VULCAN_API_EVENTS to LOGIN_METHOD_VULCAN_API + ), listOf(LOGIN_METHOD_VULCAN_API)), + // 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)), + // homework + Feature(LOGIN_TYPE_VULCAN, FEATURE_HOMEWORK, listOf( + ENDPOINT_VULCAN_API_HOMEWORK to LOGIN_METHOD_VULCAN_API + ), listOf(LOGIN_METHOD_VULCAN_API)), + // behaviour + Feature(LOGIN_TYPE_VULCAN, FEATURE_BEHAVIOUR, listOf( + ENDPOINT_VULCAN_API_NOTICES to LOGIN_METHOD_VULCAN_API + ), listOf(LOGIN_METHOD_VULCAN_API)), + // attendance + Feature(LOGIN_TYPE_VULCAN, FEATURE_ATTENDANCE, listOf( + ENDPOINT_VULCAN_API_ATTENDANCE to LOGIN_METHOD_VULCAN_API + ), listOf(LOGIN_METHOD_VULCAN_API)), + // messages + Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_INBOX, listOf( + ENDPOINT_VULCAN_API_MESSAGES_INBOX to LOGIN_METHOD_VULCAN_API + ), listOf(LOGIN_METHOD_VULCAN_API)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_SENT, listOf( + ENDPOINT_VULCAN_API_MESSAGES_SENT to LOGIN_METHOD_VULCAN_API + ), listOf(LOGIN_METHOD_VULCAN_API)), + + Feature(LOGIN_TYPE_VULCAN, FEATURE_ALWAYS_NEEDED, listOf( + ENDPOINT_VULCAN_API_STUDENT_LIST to LOGIN_METHOD_VULCAN_API, + ENDPOINT_VULCAN_API_DICTIONARIES to LOGIN_METHOD_VULCAN_API + ), listOf(LOGIN_METHOD_VULCAN_API)) + /*Feature(LOGIN_TYPE_VULCAN, FEATURE_STUDENT_INFO, listOf( + ENDPOINT_VULCAN_API to LOGIN_METHOD_VULCAN_WEB + ), listOf(LOGIN_METHOD_VULCAN_WEB)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_STUDENT_NUMBER, listOf( + ENDPOINT_VULCAN_API to LOGIN_METHOD_VULCAN_WEB + ), listOf(LOGIN_METHOD_VULCAN_WEB)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_SCHOOL_INFO, listOf( + ENDPOINT_VULCAN_API to LOGIN_METHOD_VULCAN_WEB + ), listOf(LOGIN_METHOD_VULCAN_WEB)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_CLASS_INFO, listOf( + ENDPOINT_VULCAN_API to LOGIN_METHOD_VULCAN_WEB + ), listOf(LOGIN_METHOD_VULCAN_WEB)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_TEAM_INFO, listOf( + ENDPOINT_VULCAN_API to LOGIN_METHOD_VULCAN_WEB + ), listOf(LOGIN_METHOD_VULCAN_WEB)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_TEACHERS, listOf( + ENDPOINT_VULCAN_API to LOGIN_METHOD_VULCAN_WEB + ), listOf(LOGIN_METHOD_VULCAN_WEB)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_SUBJECTS, listOf( + ENDPOINT_VULCAN_API to LOGIN_METHOD_VULCAN_WEB + ), listOf(LOGIN_METHOD_VULCAN_WEB)), + Feature(LOGIN_TYPE_VULCAN, FEATURE_CLASSROOMS, listOf( + ENDPOINT_VULCAN_API to LOGIN_METHOD_VULCAN_WEB + ), listOf(LOGIN_METHOD_VULCAN_WEB)),*/ + +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanApi.kt new file mode 100644 index 00000000..c6d4397a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanApi.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-19 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data + +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.callback.JsonCallbackHandler +import io.github.wulkanowy.signer.android.signContent +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.teams.Team +import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.net.HttpURLConnection +import java.util.* + +open class VulcanApi(open val data: DataVulcan) { + companion object { + const val TAG = "VulcanApi" + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + fun apiGet( + tag: String, + endpoint: String, + method: Int = POST, + parameters: Map = emptyMap(), + baseUrl: Boolean = false, + onSuccess: (json: JsonObject, response: Response?) -> Unit + ) { + val url = "${if (baseUrl) data.apiUrl else data.fullApiUrl}/$endpoint" + + d(tag, "Request: Vulcan/Api - $url") + + if (data.teamList.size() == 0) { + data.profile?.studentClassName?.also { name -> + val id = Utils.crc16(name.toByteArray()).toLong() + + val teamObject = Team( + profileId, + id, + name, + Team.TYPE_CLASS, + "${data.schoolName}:$name", + -1 + ) + data.teamList.put(id, teamObject) + } + } + + val finalPayload = JsonObject() + parameters.map { (name, value) -> + when (value) { + is JsonObject -> finalPayload.add(name, value) + is JsonArray -> finalPayload.add(name, value) + is String -> finalPayload.addProperty(name, value) + is Int -> finalPayload.addProperty(name, value) + is Long -> finalPayload.addProperty(name, value) + is Float -> finalPayload.addProperty(name, value) + is Char -> finalPayload.addProperty(name, value) + } + } + finalPayload.addProperty("RemoteMobileTimeKey", System.currentTimeMillis() / 1000) + finalPayload.addProperty("TimeKey", System.currentTimeMillis() / 1000 - 1) + finalPayload.addProperty("RequestId", UUID.randomUUID().toString()) + finalPayload.addProperty("RemoteMobileAppVersion", VULCAN_API_APP_VERSION) + finalPayload.addProperty("RemoteMobileAppName", VULCAN_API_APP_NAME) + + val callback = object : JsonCallbackHandler() { + override fun onSuccess(json: JsonObject?, response: Response?) { + if (json == null && response?.parserErrorBody == null) { + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + if (response?.code() ?: 200 != 200) { + when (response?.code()) { + 503 -> ERROR_VULCAN_API_MAINTENANCE + 400 -> ERROR_VULCAN_API_BAD_REQUEST + else -> ERROR_VULCAN_API_OTHER + }.let { errorCode -> + data.error(ApiError(tag, errorCode) + .withResponse(response) + .withApiResponse(json?.toString() ?: response?.parserErrorBody)) + return + } + } + + if (json == null) { + data.error(ApiError(tag, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + try { + onSuccess(json, response) + } catch (e: Exception) { + data.error(ApiError(tag, EXCEPTION_VULCAN_API_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_API_USER_AGENT) + .addHeader("RequestCertificateKey", data.apiCertificateKey) + .addHeader("RequestSignatureValue", + try { + signContent( + data.apiCertificatePrivate ?: "", + finalPayload.toString() + ) + } catch (e: Exception) {e.printStackTrace();""}) + .apply { + when (method) { + GET -> get() + POST -> post() + } + } + .setJsonBody(finalPayload) + .allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST) + .allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN) + .allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED) + .allowErrorCode(HttpURLConnection.HTTP_UNAVAILABLE) + .callback(callback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt new file mode 100644 index 00000000..378c5102 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-6. + */ + +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.utils.Utils + +class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "VulcanData" + } + + init { + nextEndpoint(onSuccess) + } + + private fun nextEndpoint(onSuccess: () -> Unit) { + if (data.targetEndpointIds.isEmpty()) { + onSuccess() + return + } + if (data.cancelled) { + onSuccess() + return + } + useEndpoint(data.targetEndpointIds.removeAt(0)) { + data.progress(data.progressStep) + nextEndpoint(onSuccess) + } + } + + private fun useEndpoint(endpointId: Int, onSuccess: () -> Unit) { + Utils.d(TAG, "Using endpoint $endpointId") + when (endpointId) { + ENDPOINT_VULCAN_API_DICTIONARIES -> { + data.startProgress(R.string.edziennik_progress_endpoint_dictionaries) + VulcanApiDictionaries(data, onSuccess) + } + ENDPOINT_VULCAN_API_GRADES -> { + data.startProgress(R.string.edziennik_progress_endpoint_grades) + VulcanApiGrades(data, onSuccess) + } + ENDPOINT_VULCAN_API_GRADES_SUMMARY -> { + data.startProgress(R.string.edziennik_progress_endpoint_proposed_grades) + VulcanApiProposedGrades(data, onSuccess) + } + ENDPOINT_VULCAN_API_EVENTS -> { + data.startProgress(R.string.edziennik_progress_endpoint_events) + VulcanApiEvents(data, isHomework = false, onSuccess = onSuccess) + } + ENDPOINT_VULCAN_API_HOMEWORK -> { + data.startProgress(R.string.edziennik_progress_endpoint_homework) + VulcanApiEvents(data, isHomework = true, onSuccess = onSuccess) + } + ENDPOINT_VULCAN_API_NOTICES -> { + data.startProgress(R.string.edziennik_progress_endpoint_notices) + VulcanApiNotices(data, onSuccess) + } + ENDPOINT_VULCAN_API_ATTENDANCE -> { + data.startProgress(R.string.edziennik_progress_endpoint_attendance) + VulcanApiAttendance(data, onSuccess) + } + ENDPOINT_VULCAN_API_TIMETABLE -> { + data.startProgress(R.string.edziennik_progress_endpoint_timetable) + VulcanApiTimetable(data, onSuccess) + } + ENDPOINT_VULCAN_API_MESSAGES_INBOX -> { + data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox) + VulcanApiMessagesInbox(data, onSuccess) + } + ENDPOINT_VULCAN_API_MESSAGES_SENT -> { + data.startProgress(R.string.edziennik_progress_endpoint_messages_outbox) + VulcanApiMessagesSent(data, onSuccess) + } + else -> onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiAttendance.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiAttendance.kt new file mode 100644 index 00000000..c2df68a1 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiAttendance.kt @@ -0,0 +1,78 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api + +import androidx.core.util.isEmpty +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_ATTENDANCE +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_API_ATTENDANCE +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance +import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance.TYPE_PRESENT +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.utils.models.Date + +class VulcanApiAttendance(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) { + companion object { + const val TAG = "VulcanApiAttendance" + } + + init { data.profile?.also { profile -> + if (data.attendanceTypes.isEmpty()) { + data.db.attendanceTypeDao().getAllNow(profileId).toSparseArray(data.attendanceTypes) { it.id } + } + + val startDate: String = profile.getSemesterStart(profile.currentSemester).stringY_m_d + val endDate: String = profile.getSemesterEnd(profile.currentSemester).stringY_m_d + + apiGet(TAG, VULCAN_API_ENDPOINT_ATTENDANCE, parameters = mapOf( + "DataPoczatkowa" to startDate, + "DataKoncowa" to endDate, + "IdOddzial" to data.studentClassId, + "IdUczen" to data.studentId, + "IdOkresKlasyfikacyjny" to data.studentSemesterId + )) { json, _ -> + json.getJsonObject("Data")?.getJsonArray("Frekwencje")?.forEach { attendanceEl -> + val attendance = attendanceEl.asJsonObject + + val attendanceCategory = data.attendanceTypes.get(attendance.getLong("IdKategoria") ?: return@forEach) + ?: return@forEach + + val type = attendanceCategory.type + + val id = (attendance.getInt("Dzien") ?: 0) + (attendance.getInt("Numer") ?: 0) + + val lessonDateMillis = Date.fromY_m_d(attendance.getString("DzienTekst")).inMillis + val lessonDate = Date.fromMillis(lessonDateMillis) + + val lessonSemester = profile.dateToSemester(lessonDate) + + val attendanceObject = Attendance( + profileId, + id.toLong(), + -1, + attendance.getLong("IdPrzedmiot") ?: -1, + lessonSemester, + attendance.getString("PrzedmiotNazwa") + attendanceCategory.name.let { " - $it" }, + lessonDate, + data.lessonRanges.get(attendance.getInt("Numer") ?: 0)?.startTime, + type) + + data.attendanceList.add(attendanceObject) + if (attendanceObject.type != TYPE_PRESENT) { + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_ATTENDANCE, + attendanceObject.id, + profile.empty, + profile.empty, + attendanceObject.lessonDate.combineWith(attendanceObject.startTime) + )) + } + } + + data.setSyncNext(ENDPOINT_VULCAN_API_ATTENDANCE, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess()} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiDictionaries.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiDictionaries.kt new file mode 100644 index 00000000..b5ef18e1 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiDictionaries.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-20 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_DICTIONARIES +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_API_DICTIONARIES +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi +import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance +import pl.szczodrzynski.edziennik.data.db.modules.attendance.AttendanceType +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory +import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonRange +import pl.szczodrzynski.edziennik.data.db.modules.notices.NoticeType +import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.utils.models.Time + +class VulcanApiDictionaries(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) { + companion object { + const val TAG = "VulcanApiDictionaries" + } + + init { + apiGet(TAG, VULCAN_API_ENDPOINT_DICTIONARIES) { json, _ -> + val elements = json.getJsonObject("Data") + + elements?.getJsonArray("Pracownicy")?.forEach { saveTeacher(it.asJsonObject) } + elements?.getJsonArray("Przedmioty")?.forEach { saveSubject(it.asJsonObject) } + elements?.getJsonArray("PoryLekcji")?.forEach { saveLessonRange(it.asJsonObject) } + elements?.getJsonArray("KategorieOcen")?.forEach { saveGradeCategory(it.asJsonObject) } + elements?.getJsonArray("KategorieUwag")?.forEach { saveNoticeType(it.asJsonObject) } + elements?.getJsonArray("KategorieFrekwencji")?.forEach { saveAttendanceType(it.asJsonObject) } + + data.setSyncNext(ENDPOINT_VULCAN_API_DICTIONARIES, 4 * DAY) + onSuccess() + } + } + + private fun saveTeacher(teacher: JsonObject) { + val id = teacher.getLong("Id") ?: return + val name = teacher.getString("Imie") ?: "" + val surname = teacher.getString("Nazwisko") ?: "" + val loginId = teacher.getString("LoginId") ?: "-1" + + val teacherObject = Teacher( + profileId, + id, + name, + surname, + loginId + ) + + data.teacherList.put(id, teacherObject) + } + + private fun saveSubject(subject: JsonObject) { + val id = subject.getLong("Id") ?: return + val longName = subject.getString("Nazwa") ?: "" + val shortName = subject.getString("Kod") ?: "" + + val subjectObject = Subject( + profileId, + id, + longName, + shortName + ) + + data.subjectList.put(id, subjectObject) + } + + private fun saveLessonRange(lessonRange: JsonObject) { + val lessonNumber = lessonRange.getInt("Numer") ?: return + val startTime = lessonRange.getString("PoczatekTekst")?.let { Time.fromH_m(it) } ?: return + val endTime = lessonRange.getString("KoniecTekst")?.let { Time.fromH_m(it) } ?: return + + val lessonRangeObject = LessonRange( + profileId, + lessonNumber, + startTime, + endTime + ) + + data.lessonRanges.put(lessonNumber, lessonRangeObject) + } + + private fun saveGradeCategory(gradeCategory: JsonObject) { + val id = gradeCategory.getLong("Id") ?: return + val name = gradeCategory.getString("Nazwa") ?: "" + + val gradeCategoryObject = GradeCategory( + profileId, + id, + 0.0f, + -1, + name + ) + + data.gradeCategories.put(id, gradeCategoryObject) + } + + private fun saveNoticeType(noticeType: JsonObject) { + val id = noticeType.getLong("Id") ?: return + val name = noticeType.getString("Nazwa") ?: "" + + val noticeTypeObject = NoticeType( + profileId, + id, + name + ) + + data.noticeTypes.put(id, noticeTypeObject) + } + + private fun saveAttendanceType(attendanceType: JsonObject) { + val id = attendanceType.getLong("Id") ?: return + val name = attendanceType.getString("Nazwa") ?: "" + + val absent = attendanceType.getBoolean("Nieobecnosc") ?: false + val excused = attendanceType.getBoolean("Usprawiedliwione") ?: false + val type = if (absent) { + if (excused) + Attendance.TYPE_ABSENT_EXCUSED + else + Attendance.TYPE_ABSENT + } else { + val belated = attendanceType.getBoolean("Spoznienie") ?: false + val released = attendanceType.getBoolean("Zwolnienie") ?: false + val present = attendanceType.getBoolean("Obecnosc") ?: true + if (belated) + if (excused) + Attendance.TYPE_ABSENT_EXCUSED + else + Attendance.TYPE_ABSENT + else if (released) + Attendance.TYPE_RELEASED + else if (present) + Attendance.TYPE_PRESENT + else + Attendance.TYPE_CUSTOM + } + + val attendanceTypeObject = AttendanceType( + profileId, + id, + name, + type, + -1 + ) + + data.attendanceTypes.put(id, attendanceTypeObject) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiEvents.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiEvents.kt new file mode 100644 index 00000000..045c80f7 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiEvents.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-20 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api + +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_EVENTS +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_HOMEWORK +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_API_EVENTS +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_API_HOMEWORK +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.getBoolean +import pl.szczodrzynski.edziennik.getJsonArray +import pl.szczodrzynski.edziennik.getLong +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.utils.models.Date + +class VulcanApiEvents(override val data: DataVulcan, private val isHomework: Boolean, val onSuccess: () -> Unit) : VulcanApi(data) { + companion object { + const val TAG = "VulcanApiEvents" + } + + init { data.profile?.also { profile -> + + val startDate: String = when (profile.empty) { + true -> profile.getSemesterStart(profile.currentSemester).stringY_m_d + else -> Date.getToday().stepForward(0, -1, 0).stringY_m_d + } + val endDate: String = profile.getSemesterEnd(profile.currentSemester).stringY_m_d + + val endpoint = when (isHomework) { + true -> VULCAN_API_ENDPOINT_HOMEWORK + else -> VULCAN_API_ENDPOINT_EVENTS + } + apiGet(TAG, endpoint, parameters = mapOf( + "DataPoczatkowa" to startDate, + "DataKoncowa" to endDate, + "IdOddzial" to data.studentClassId, + "IdUczen" to data.studentId, + "IdOkresKlasyfikacyjny" to data.studentSemesterId + )) { json, _ -> + val events = json.getJsonArray("Data") + + events?.forEach { eventEl -> + val event = eventEl.asJsonObject + + val id = event?.getLong("Id") ?: return@forEach + val eventDate = Date.fromY_m_d(event.getString("DataTekst") ?: return@forEach) + val subjectId = event.getLong("IdPrzedmiot") ?: -1 + val teacherId = event.getLong("IdPracownik") ?: -1 + val topic = event.getString("Opis") ?: "" + + val lessonList = data.db.timetableDao().getForDateNow(profileId, eventDate) + val startTime = lessonList.firstOrNull { it.subjectId == subjectId }?.startTime + + val type = when (isHomework) { + true -> Event.TYPE_HOMEWORK + else -> when (event.getBoolean("Rodzaj")) { + false -> Event.TYPE_SHORT_QUIZ + else -> Event.TYPE_EXAM + } + } + val teamId = event.getLong("IdOddzial") ?: data.teamClass?.id ?: -1 + + val eventObject = Event( + profileId, + id, + eventDate, + startTime, + topic, + -1, + type, + false, + teacherId, + subjectId, + teamId + ) + + data.eventList.add(eventObject) + data.metadataList.add(Metadata( + profileId, + if (isHomework) Metadata.TYPE_HOMEWORK else Metadata.TYPE_EVENT, + id, + profile.empty, + profile.empty, + System.currentTimeMillis() + )) + } + + when (isHomework) { + true -> { + data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_HOMEWORK)) + data.setSyncNext(ENDPOINT_VULCAN_API_HOMEWORK, SYNC_ALWAYS) + } + false -> { + data.toRemove.add(DataRemoveModel.Events.futureExceptType(Event.TYPE_HOMEWORK)) + data.setSyncNext(ENDPOINT_VULCAN_API_EVENTS, SYNC_ALWAYS) + } + } + onSuccess() + } + } ?: onSuccess()} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiGrades.kt new file mode 100644 index 00000000..93528a28 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiGrades.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-19 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_API_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import java.text.DecimalFormat +import kotlin.math.roundToInt + +class VulcanApiGrades(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) { + companion object { + const val TAG = "VulcanApiGrades" + } + + init { data.profile?.also { profile -> + + apiGet(TAG, VULCAN_API_ENDPOINT_GRADES, parameters = mapOf( + "IdUczen" to data.studentId, + "IdOkresKlasyfikacyjny" to data.studentSemesterId + )) { json, _ -> + val grades = json.getJsonArray("Data") + + grades?.forEach { gradeEl -> + val grade = gradeEl.asJsonObject + + val id = grade.getLong("Id") ?: return@forEach + val categoryId = grade.getLong("IdKategoria") ?: -1 + val category = data.gradeCategories.singleOrNull{ it.categoryId == categoryId }?.text + ?: "" + val teacherId = grade.getLong("IdPracownikD") ?: -1 + val subjectId = grade.getLong("IdPrzedmiot") ?: -1 + val description = grade.getString("Opis") + val comment = grade.getString("Komentarz") + var value = grade.getFloat("Wartosc") + var weight = grade.getFloat("WagaOceny") ?: 0.0f + val modificatorValue = grade.getFloat("WagaModyfikatora") + val numerator = grade.getFloat("Licznik") + val denominator = grade.getFloat("Mianownik") + val addedDate = (grade.getLong("DataModyfikacji") ?: return@forEach) * 1000 + + 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) modificatorValue?.also { value += it } + else weight = 0.0f + + grade.getString("Wpis") ?: "" + } + } + + comment?.also { + if (name == "") name = it + else finalDescription = (if (finalDescription == "") "" else " ") + it + } + + description?.also { + finalDescription = (if (finalDescription == "") "" else " - ") + it + } + + val color = 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() + + val gradeObject = Grade( + profileId, + id, + category, + color, + finalDescription, + name, + value ?: 0.0f, + weight, + data.studentSemesterNumber, + teacherId, + subjectId + ) + + data.gradeList.add(gradeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_GRADE, + id, + profile.empty, + profile.empty, + addedDate + + )) + } + + data.toRemove.add(DataRemoveModel.Grades.semesterWithType(data.studentSemesterNumber, Grade.TYPE_NORMAL)) + data.setSyncNext(ENDPOINT_VULCAN_API_GRADES, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess()} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesChangeStatus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesChangeStatus.kt new file mode 100644 index 00000000..9cc2bf6b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesChangeStatus.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-11-12 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api + +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_MESSAGES_CHANGE_STATUS +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata + +class VulcanApiMessagesChangeStatus( + override val data: DataVulcan, + private val messageObject: MessageFull, + val onSuccess: () -> Unit +) : VulcanApi(data) { + companion object { + const val TAG = "VulcanApiMessagesChangeStatus" + } + + init { + data.profile?.also { profile -> + apiGet(TAG, VULCAN_API_ENDPOINT_MESSAGES_CHANGE_STATUS, parameters = mapOf( + "WiadomoscId" to messageObject.id, + "FolderWiadomosci" to "Odebrane", + "Status" to "Widoczna", + "LoginId" to data.studentLoginId, + "IdUczen" to data.studentId + )) { _, _ -> + + if (!messageObject.seen) { + data.setSeenMetadataList.add(Metadata( + profileId, + Metadata.TYPE_MESSAGE, + messageObject.id, + true, + true, + messageObject.addedDate + )) + } + + if (messageObject.type != TYPE_SENT) { + val messageRecipientObject = MessageRecipient( + profileId, + -1, + -1, + System.currentTimeMillis(), + messageObject.id + ) + + data.messageRecipientList.add(messageRecipientObject) + } + + onSuccess() + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesInbox.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesInbox.kt new file mode 100644 index 00000000..3ac292db --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesInbox.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-11-01 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_MESSAGES_RECEIVED +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_API_MESSAGES_INBOX +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.models.Date +import kotlin.text.replace + +class VulcanApiMessagesInbox(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) { + companion object { + const val TAG = "VulcanApiMessagesInbox" + } + + init { + data.profile?.also { profile -> + + val startDate = when (profile.empty) { + true -> profile.getSemesterStart(profile.currentSemester).inUnix + else -> Date.getToday().stepForward(0, -2, 0).inUnix + } + val endDate = Date.getToday().stepForward(0, 1, 0).inUnix + + apiGet(TAG, VULCAN_API_ENDPOINT_MESSAGES_RECEIVED, parameters = mapOf( + "DataPoczatkowa" to startDate, + "DataKoncowa" to endDate, + "LoginId" to data.studentLoginId, + "IdUczen" to data.studentId + )) { json, _ -> + json.getJsonArray("Data").asJsonObjectList()?.forEach { message -> + val id = message.getLong("WiadomoscId") ?: return@forEach + val subject = message.getString("Tytul") ?: "" + val body = message.getString("Tresc") ?: "" + + val senderLoginId = message.getString("NadawcaId") ?: return@forEach + val senderId = data.teacherList + .singleOrNull { it.loginId == senderLoginId }?.id ?: { + + val senderName = message.getString("Nadawca") ?: "" + + senderName.splitName()?.let { (senderLastName, senderFirstName) -> + val teacherObject = Teacher( + profileId, + -1 * Utils.crc16(senderName.toByteArray()).toLong(), + senderFirstName, + senderLastName, + senderLoginId + ) + data.teacherList.put(teacherObject.id, teacherObject) + teacherObject.id + } + }.invoke() ?: -1 + + val sentDate = message.getLong("DataWyslaniaUnixEpoch")?.let { it * 1000 } + ?: -1 + val readDate = message.getLong("DataPrzeczytaniaUnixEpoch")?.let { it * 1000 } + ?: -1 + + val messageObject = Message( + profileId, + id, + subject, + body.replace("\n", "
    "), + TYPE_RECEIVED, + senderId, + -1 + ) + + val messageRecipientObject = MessageRecipient( + profileId, + -1, + -1, + readDate, + id + ) + + data.messageIgnoreList.add(messageObject) + data.messageRecipientList.add(messageRecipientObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_MESSAGE, + id, + readDate > 0, + readDate > 0, + sentDate + )) + } + + data.setSyncNext(ENDPOINT_VULCAN_API_MESSAGES_INBOX, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesSent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesSent.kt new file mode 100644 index 00000000..a0fd55b3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiMessagesSent.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-11-5 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api + +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_MESSAGES_SENT +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_API_MESSAGES_SENT +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.models.Date +import kotlin.text.replace + +class VulcanApiMessagesSent(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) { + companion object { + const val TAG = "VulcanApiMessagesSent" + } + + init { + data.profile?.also { profile -> + + val startDate = when (profile.empty) { + true -> profile.getSemesterStart(profile.currentSemester).inUnix + else -> Date.getToday().stepForward(0, -2, 0).inUnix + } + val endDate = Date.getToday().stepForward(0, 1, 0).inUnix + + apiGet(TAG, VULCAN_API_ENDPOINT_MESSAGES_SENT, parameters = mapOf( + "DataPoczatkowa" to startDate, + "DataKoncowa" to endDate, + "LoginId" to data.studentLoginId, + "IdUczen" to data.studentId + )) { json, _ -> + json.getJsonArray("Data")?.asJsonObjectList()?.forEach { message -> + val id = message.getLong("WiadomoscId") ?: return@forEach + val subject = message.getString("Tytul") ?: "" + val body = message.getString("Tresc") ?: "" + val readBy = message.getInt("Przeczytane") ?: 0 + val unreadBy = message.getInt("Nieprzeczytane") ?: 0 + val sentDate = message.getLong("DataWyslaniaUnixEpoch")?.let { it * 1000 } ?: -1 + + message.getJsonArray("Adresaci")?.asJsonObjectList() + ?.onEach { receiver -> + + val receiverLoginId = receiver.getString("LoginId") + ?: return@onEach + val receiverId = data.teacherList.singleOrNull { it.loginId == receiverLoginId }?.id + ?: { + val receiverName = receiver.getString("Nazwa") ?: "" + + receiverName.splitName()?.let { (receiverLastName, receiverFirstName) -> + val teacherObject = Teacher( + profileId, + -1 * Utils.crc16(receiverName.toByteArray()).toLong(), + receiverFirstName, + receiverLastName, + receiverLoginId + ) + data.teacherList.put(teacherObject.id, teacherObject) + teacherObject.id + } + }.invoke() ?: -1 + + val readDate: Long = when (readBy) { + 0 -> 0 + else -> when (unreadBy) { + 0 -> 1 + else -> -1 + } + } + + val messageRecipientObject = MessageRecipient( + profileId, + receiverId, + -1, + readDate, + id + ) + + data.messageRecipientList.add(messageRecipientObject) + } + + val messageObject = Message( + profileId, + id, + subject, + body.replace("\n", "
    "), + TYPE_SENT, + -1, + -1 + ) + + data.messageIgnoreList.add(messageObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_MESSAGE, + id, + true, + true, + sentDate + )) + } + + data.setSyncNext(ENDPOINT_VULCAN_API_MESSAGES_SENT, 1 * DAY, DRAWER_ITEM_MESSAGES) + onSuccess() + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiNotices.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiNotices.kt new file mode 100644 index 00000000..eebe6c29 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiNotices.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-23 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api + +import androidx.core.util.isEmpty +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_NOTICES +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_API_NOTICES +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.notices.Notice +import pl.szczodrzynski.edziennik.getJsonArray +import pl.szczodrzynski.edziennik.getLong +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.toSparseArray +import pl.szczodrzynski.edziennik.utils.models.Date + +class VulcanApiNotices(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) { + companion object { + const val TAG = "VulcanApiNotices" + } + + init { data.profile?.also { profile -> + if (data.noticeTypes.isEmpty()) { + data.db.noticeTypeDao().getAllNow(profileId).toSparseArray(data.noticeTypes) { it.id } + } + + apiGet(TAG, VULCAN_API_ENDPOINT_NOTICES, parameters = mapOf( + "IdUczen" to data.studentId, + "IdOkresKlasyfikacyjny" to data.studentSemesterId + )) { json, _ -> + json.getJsonArray("Data")?.forEach { noticeEl -> + val notice = noticeEl.asJsonObject + + val id = notice.getLong("Id") ?: return@forEach + val text = notice.getString("TrescUwagi") ?: return@forEach + val teacherId = notice.getLong("IdPracownik") ?: -1 + val addedDate = Date.fromY_m_d(notice.getString("DataWpisuTekst")).inMillis + + val noticeObject = Notice( + profileId, + id, + text, + profile.currentSemester, + Notice.TYPE_NEUTRAL, + teacherId + ) + + data.noticeList.add(noticeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_NOTICE, + id, + profile.empty, + profile.empty, + addedDate + )) + } + + data.setSyncNext(ENDPOINT_VULCAN_API_NOTICES, SYNC_ALWAYS) + onSuccess() + } + } ?: onSuccess()} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiProposedGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiProposedGrades.kt new file mode 100644 index 00000000..c647af2b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiProposedGrades.kt @@ -0,0 +1,81 @@ +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api + +import com.google.gson.JsonArray +import pl.szczodrzynski.edziennik.HOUR +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_GRADES_PROPOSITIONS +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_API_GRADES_SUMMARY +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi +import pl.szczodrzynski.edziennik.asJsonObjectList +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.getJsonArray +import pl.szczodrzynski.edziennik.getJsonObject +import pl.szczodrzynski.edziennik.utils.Utils + +class VulcanApiProposedGrades(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) { + companion object { + const val TAG = "VulcanApiProposedGrades" + } + + init { data.profile?.also { profile -> + + apiGet(TAG, VULCAN_API_ENDPOINT_GRADES_PROPOSITIONS, parameters = mapOf( + "IdUczen" to data.studentId, + "IdOkresKlasyfikacyjny" to data.studentSemesterId + )) { json, _ -> + val grades = json.getJsonObject("Data") + + grades.getJsonArray("OcenyPrzewidywane")?.let { + processGradeList(it, isFinal = false) + } + + grades.getJsonArray("OcenyKlasyfikacyjne")?.let { + processGradeList(it, isFinal = true) + } + + data.setSyncNext(ENDPOINT_VULCAN_API_GRADES_SUMMARY, 6*HOUR) + onSuccess() + } + } ?: onSuccess()} + + private fun processGradeList(grades: JsonArray, isFinal: Boolean) { + grades.asJsonObjectList()?.forEach { grade -> + val name = grade.get("Wpis").asString + val value = Utils.getGradeValue(name) + val subjectId = grade.get("IdPrzedmiot").asLong + + val id = subjectId * -100 - data.studentSemesterNumber + + val color = Utils.getVulcanGradeColor(name) + + val gradeObject = Grade( + profileId, + id, + "", + color, + "", + name, + value, + 0f, + data.studentSemesterNumber, + -1, + subjectId + ) + if (data.studentSemesterNumber == 1) { + gradeObject.type = if (isFinal) Grade.TYPE_SEMESTER1_FINAL else Grade.TYPE_SEMESTER1_PROPOSED + } else { + gradeObject.type = if (isFinal) Grade.TYPE_SEMESTER2_FINAL else Grade.TYPE_SEMESTER2_PROPOSED + } + data.gradeList.add(gradeObject) + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_GRADE, + gradeObject.id, + data.profile?.empty ?: false, + data.profile?.empty ?: false, + System.currentTimeMillis() + )) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiSendMessage.kt new file mode 100644 index 00000000..2a76ab1d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiSendMessage.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-29. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_MESSAGES_ADD +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.events.MessageSentEvent +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher + +class VulcanApiSendMessage( + override val data: DataVulcan, + val recipients: List, + val subject: String, + val text: String, + val onSuccess: () -> Unit +) : VulcanApi(data) { + companion object { + private const val TAG = "VulcanApiSendMessage" + } + + init { + val recipientsArray = JsonArray() + for (teacher in recipients) { + teacher.loginId?.let { + recipientsArray += JsonObject( + "LoginId" to it, + "Nazwa" to "${teacher.fullNameLastFirst} - pracownik" + ) + } + } + val params = mapOf( + "NadawcaWiadomosci" to (profile?.accountNameLong ?: profile?.studentNameLong ?: ""), + "Tytul" to subject, + "Tresc" to text, + "Adresaci" to recipientsArray, + "LoginId" to data.studentLoginId, + "IdUczen" to data.studentId + ) + + apiGet(TAG, VULCAN_API_ENDPOINT_MESSAGES_ADD, parameters = params) { json, _ -> + val messageId = json.getJsonObject("Data").getLong("WiadomoscId") + + if (messageId == null) { + // TODO error + return@apiGet + } + + VulcanApiMessagesSent(data) { + val message = data.messageIgnoreList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject } + val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == messageId } + val event = MessageSentEvent(data.profileId, message, metadata?.addedDate) + + EventBus.getDefault().postSticky(event) + onSuccess() + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiTemplate.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiTemplate.kt new file mode 100644 index 00000000..adda3dd0 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiTemplate.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-20 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api + +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi + +class VulcanApiTemplate(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) { + companion object { + const val TAG = "VulcanApi" + } + + init { + /* data.profile?.also { profile -> + apiGet(TAG, VULCAN_API_ENDPOINT_) { json, _ -> + + data.setSyncNext(ENDPOINT_VULCAN_API_, SYNC_ALWAYS) + onSuccess() + } + } */ + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiTimetable.kt new file mode 100644 index 00000000..c2ec8f97 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/api/VulcanApiTimetable.kt @@ -0,0 +1,213 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-11-13 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api + +import androidx.core.util.set +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.Regexes.VULCAN_SHIFT_ANNOTATION +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_TIMETABLE +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_API_TIMETABLE +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject +import pl.szczodrzynski.edziennik.data.db.modules.teams.Team +import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson +import pl.szczodrzynski.edziennik.utils.Utils.crc16 +import pl.szczodrzynski.edziennik.utils.Utils.d +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Week + +class VulcanApiTimetable(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) { + companion object { + const val TAG = "VulcanApiTimetable" + } + + init { data.profile?.also { profile -> + val currentWeekStart = Week.getWeekStart() + + if (Date.getToday().weekDay > 4) { + currentWeekStart.stepForward(0, 0, 7) + } + + val getDate = data.arguments?.getString("weekStart") ?: currentWeekStart.stringY_m_d + + val weekStart = Date.fromY_m_d(getDate) + val weekEnd = weekStart.clone().stepForward(0, 0, 6) + + apiGet(TAG, VULCAN_API_ENDPOINT_TIMETABLE, parameters = mapOf( + "DataPoczatkowa" to weekStart.stringY_m_d, + "DataKoncowa" to weekEnd.stringY_m_d, + "IdUczen" to data.studentId, + "IdOddzial" to data.studentClassId, + "IdOkresKlasyfikacyjny" to data.studentSemesterId + )) { json, _ -> + val dates = mutableSetOf() + val lessons = mutableListOf() + + json.getJsonArray("Data")?.asJsonObjectList()?.forEach { lesson -> + if (lesson.getBoolean("PlanUcznia") != true) + return@forEach + val lessonDate = Date.fromY_m_d(lesson.getString("DzienTekst")) + val lessonNumber = lesson.getInt("NumerLekcji") + val lessonRange = data.lessonRanges.singleOrNull { it.lessonNumber == lessonNumber } + val startTime = lessonRange?.startTime + val endTime = lessonRange?.endTime + val teacherId = lesson.getLong("IdPracownik") + val classroom = lesson.getString("Sala") + + val oldTeacherId = lesson.getLong("IdPracownikOld") + + val changeAnnotation = lesson.getString("AdnotacjaOZmianie") ?: "" + val type = when { + changeAnnotation.startsWith("(przeniesiona z") -> Lesson.TYPE_SHIFTED_TARGET + changeAnnotation.startsWith("(przeniesiona na") -> Lesson.TYPE_SHIFTED_SOURCE + changeAnnotation.startsWith("(zastępstwo") -> Lesson.TYPE_CHANGE + lesson.getBoolean("PrzekreslonaNazwa") == true -> Lesson.TYPE_CANCELLED + else -> Lesson.TYPE_NORMAL + } + + val teamId = lesson.getString("PodzialSkrot")?.let { teamName -> + val name = "${data.teamClass?.name} $teamName" + val id = name.crc16().toLong() + var team = data.teamList.singleOrNull { it.name == name } + if (team == null) { + team = Team( + profileId, + id, + name, + Team.TYPE_VIRTUAL, + "${data.schoolName}:$name", + teacherId ?: oldTeacherId ?: -1 + ) + data.teamList[id] = team + } + team.id + } ?: data.studentClassId.toLong() + + val subjectId = lesson.getLong("IdPrzedmiot")?.let { + when (it) { + 0L -> { + val subjectName = lesson.getString("PrzedmiotNazwa") ?: "" + + data.subjectList.singleOrNull { subject -> subject.longName == subjectName }?.id + ?: { + /** + * CREATE A NEW SUBJECT IF IT DOESN'T EXIST + */ + + val subjectObject = Subject( + profileId, + -1 * crc16(subjectName.toByteArray()).toLong(), + subjectName, + subjectName + ) + data.subjectList.put(subjectObject.id, subjectObject) + subjectObject.id + }.invoke() + } + else -> it + } + } + + val lessonObject = Lesson(profileId, -1).apply { + this.type = type + + when (type) { + Lesson.TYPE_NORMAL, Lesson.TYPE_CHANGE, Lesson.TYPE_SHIFTED_TARGET -> { + this.date = lessonDate + this.lessonNumber = lessonNumber + this.startTime = startTime + this.endTime = endTime + this.subjectId = subjectId + this.teacherId = teacherId + this.teamId = teamId + this.classroom = classroom + + this.oldTeacherId = oldTeacherId + } + + Lesson.TYPE_CANCELLED, Lesson.TYPE_SHIFTED_SOURCE -> { + this.oldDate = lessonDate + this.oldLessonNumber = lessonNumber + this.oldStartTime = startTime + this.oldEndTime = endTime + this.oldSubjectId = subjectId + this.oldTeacherId = teacherId + this.oldTeamId = teamId + this.oldClassroom = classroom + } + } + + if (type == Lesson.TYPE_SHIFTED_SOURCE || type == Lesson.TYPE_SHIFTED_TARGET) { + val shift = VULCAN_SHIFT_ANNOTATION.find(changeAnnotation) + val oldLessonNumber = shift?.get(2)?.toInt() + val oldLessonDate = shift?.get(3)?.let { Date.fromd_m_Y(it) } + + val oldLessonRange = data.lessonRanges.singleOrNull { it.lessonNumber == oldLessonNumber } + val oldStartTime = oldLessonRange?.startTime + val oldEndTime = oldLessonRange?.endTime + + when (type) { + Lesson.TYPE_SHIFTED_SOURCE -> { + this.lessonNumber = oldLessonNumber + this.date = oldLessonDate + this.startTime = oldStartTime + this.endTime = oldEndTime + } + + Lesson.TYPE_SHIFTED_TARGET -> { + this.oldLessonNumber = oldLessonNumber + this.oldDate = oldLessonDate + this.oldStartTime = oldStartTime + this.oldEndTime = oldEndTime + } + } + } + + this.id = buildId() + } + + val seen = profile.empty || lessonDate < Date.getToday() + + if (type != Lesson.TYPE_NORMAL) { + data.metadataList.add(Metadata( + profileId, + Metadata.TYPE_LESSON_CHANGE, + lessonObject.id, + seen, + seen, + System.currentTimeMillis() + )) + } + + dates.add(lessonDate.value) + lessons.add(lessonObject) + } + + val date: Date = weekStart.clone() + while (date <= weekEnd) { + if (!dates.contains(date.value)) { + lessons.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 ${weekStart.stringY_m_d} and ${weekEnd.stringY_m_d} - timetable downloaded for $getDate") + + data.lessonNewList.addAll(lessons) + data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd)) + + data.setSyncNext(ENDPOINT_VULCAN_API_TIMETABLE, SYNC_ALWAYS) + onSuccess() + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt new file mode 100644 index 00000000..b82388d1 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-10-19 + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.firstlogin + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.ERROR_NO_STUDENTS_IN_ACCOUNT +import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_STUDENT_LIST +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.login.VulcanLoginApi +import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.utils.models.Date + +class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { + companion object { + const val TAG = "VulcanFirstLogin" + } + + private val api = VulcanApi(data) + private val profileList = mutableListOf() + + init { + VulcanLoginApi(data) { + api.apiGet(TAG, VULCAN_API_ENDPOINT_STUDENT_LIST, baseUrl = true) { json, response -> + val students = json.getJsonArray("Data") + + if (students == null || students.size() < 1) { + data.error(ApiError(TAG, ERROR_NO_STUDENTS_IN_ACCOUNT) + .withResponse(response) + .withApiResponse(json)) + return@apiGet + } + + students.forEach { studentEl -> + val student = studentEl.asJsonObject + + val schoolSymbol = student.getString("JednostkaSprawozdawczaSymbol") ?: return@forEach + val schoolName = "${data.symbol}_$schoolSymbol" + val studentId = student.getInt("Id") ?: return@forEach + val studentLoginId = student.getInt("UzytkownikLoginId") ?: return@forEach + val studentClassId = student.getInt("IdOddzial") ?: return@forEach + val studentClassName = student.getString("OkresPoziom").toString() + (student.getString("OddzialSymbol") ?: return@forEach) + val studentSemesterId = student.getInt("IdOkresKlasyfikacyjny") ?: return@forEach + val studentFirstName = student.getString("Imie") ?: "" + val studentLastName = student.getString("Nazwisko") ?: "" + val studentNameLong = "$studentFirstName $studentLastName".fixName() + val studentNameShort = "$studentFirstName ${studentLastName[0]}.".fixName() + + val userLogin = student.getString("UzytkownikLogin") ?: "" + val currentSemesterStartDate = student.getLong("OkresDataOd") ?: return@forEach + val currentSemesterEndDate = (student.getLong("OkresDataDo") + ?: return@forEach) + 86400 + val studentSemesterNumber = student.getInt("OkresNumer") ?: return@forEach + + val newProfile = Profile() + newProfile.empty = true + + val isParent = student.getString("UzytkownikRola") == "opiekun" + val userName = if (isParent) + student.getString("UzytkownikNazwa")?.swapFirstLastName()?.fixName() + else + null + newProfile.accountNameLong = userName + newProfile.studentClassName = studentClassName + val today = Date.getToday() + newProfile.studentSchoolYear = "${today.year}/${today.year+1}" + + newProfile.putStudentData("studentId", studentId) + newProfile.putStudentData("studentLoginId", studentLoginId) + newProfile.putStudentData("studentClassId", studentClassId) + newProfile.putStudentData("studentSemesterId", studentSemesterId) + newProfile.putStudentData("schoolSymbol", schoolSymbol) + newProfile.putStudentData("schoolName", schoolName) + newProfile.putStudentData("currentSemesterEndDate", currentSemesterEndDate) + newProfile.putStudentData("studentSemesterNumber", studentSemesterNumber) + + when (studentSemesterNumber) { + 1 -> { + newProfile.dateSemester1Start = Date.fromMillis(currentSemesterStartDate * 1000) + newProfile.dateSemester2Start = Date.fromMillis(currentSemesterEndDate * 1000) + } + 2 -> { + newProfile.dateSemester2Start = Date.fromMillis(currentSemesterStartDate * 1000) + newProfile.dateYearEnd = Date.fromMillis(currentSemesterEndDate * 1000) + } + } + + newProfile.studentNameLong = studentNameLong + newProfile.studentNameShort = studentNameShort + newProfile.name = studentNameLong + newProfile.subname = userLogin + + profileList.add(newProfile) + } + + EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore)) + onSuccess() + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt new file mode 100644 index 00000000..4c11caf3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-6. + */ + +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.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.utils.Utils + +class VulcanLogin(val data: DataVulcan, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "VulcanLogin" + } + + private var cancelled = false + + init { + nextLoginMethod(onSuccess) + } + + private fun nextLoginMethod(onSuccess: () -> Unit) { + if (data.targetLoginMethodIds.isEmpty()) { + onSuccess() + return + } + if (cancelled) { + onSuccess() + return + } + useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId -> + data.progress(data.progressStep) + if (usedMethodId != -1) + data.loginMethods.add(usedMethodId) + nextLoginMethod(onSuccess) + } + } + + private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) { + // this should never be true + if (data.loginMethods.contains(loginMethodId)) { + onSuccess(-1) + return + } + Utils.d(TAG, "Using login method $loginMethodId") + when (loginMethodId) { + LOGIN_METHOD_VULCAN_API -> { + data.startProgress(R.string.edziennik_progress_login_vulcan_api) + VulcanLoginApi(data) { onSuccess(loginMethodId) } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginApi.kt new file mode 100644 index 00000000..dfa1c2ac --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginApi.kt @@ -0,0 +1,153 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-6. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login + +import android.os.Build +import com.google.gson.JsonObject +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.callback.JsonCallbackHandler +import io.github.wulkanowy.signer.android.getPrivateKeyFromCert +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.currentTimeUnix +import pl.szczodrzynski.edziennik.getJsonObject +import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.net.HttpURLConnection.HTTP_BAD_REQUEST +import java.util.* +import java.util.regex.Pattern + +class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "VulcanLoginApi" + } + + init { run { + if (data.profile != null && data.isApiLoginValid()) { + onSuccess() + } + else { + if (data.apiCertificatePfx.isNotNullNorEmpty()) { + try { + data.apiCertificatePrivate = getPrivateKeyFromCert( + if (data.apiToken?.get(0) == 'F') VULCAN_API_PASSWORD_FAKELOG else VULCAN_API_PASSWORD, + data.apiCertificatePfx ?: "" + ) + data.loginStore.removeLoginData("certificatePfx") + } catch (e: Throwable) { + e.printStackTrace() + } finally { + onSuccess() + return@run + } + } + if (data.symbol.isNotNullNorEmpty() && data.apiToken.isNotNullNorEmpty() && data.apiPin.isNotNullNorEmpty()) { + loginWithToken() + } + else { + data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) + } + } + }} + + private fun loginWithToken() { + d(TAG, "Request: Vulcan/Login/Api - ${data.apiUrl}/$VULCAN_API_ENDPOINT_CERTIFICATE") + + val callback = object : JsonCallbackHandler() { + override fun onSuccess(json: JsonObject?, response: Response?) { + if (json == null) { + if (response?.code() == HTTP_BAD_REQUEST) { + data.error(TAG, ERROR_LOGIN_VULCAN_INVALID_SYMBOL, response) + return + } + data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) + .withResponse(response)) + return + } + + var tokenStatus = json.getString("TokenStatus") + if (tokenStatus == "Null" || tokenStatus == "CertGenerated") + tokenStatus = null + val error = tokenStatus ?: json.getString("Message") + error?.let { code -> + when (code) { + "TokenNotFound" -> ERROR_LOGIN_VULCAN_INVALID_TOKEN + "TokenDead" -> ERROR_LOGIN_VULCAN_EXPIRED_TOKEN + "WrongPIN" -> { + Pattern.compile("Liczba pozostałych prób: ([0-9])", Pattern.DOTALL).matcher(tokenStatus).let { matcher -> + if (matcher.matches()) + ERROR_LOGIN_VULCAN_INVALID_PIN + 1 + matcher.group(1).toInt() + else + ERROR_LOGIN_VULCAN_INVALID_PIN + } + } + "Broken" -> ERROR_LOGIN_VULCAN_INVALID_PIN_0_REMAINING + "OnlyKindergarten" -> ERROR_LOGIN_VULCAN_ONLY_KINDERGARTEN + "NoPupils" -> ERROR_LOGIN_VULCAN_NO_PUPILS + else -> ERROR_LOGIN_VULCAN_OTHER + }.let { errorCode -> + data.error(ApiError(TAG, errorCode) + .withApiResponse(json) + .withResponse(response)) + return + } + } + + val cert = json.getJsonObject("TokenCert") + if (cert == null) { + data.error(ApiError(TAG, ERROR_LOGIN_VULCAN_OTHER) + .withApiResponse(json) + .withResponse(response)) + return + } + + data.apiCertificateKey = cert.getString("CertyfikatKlucz") + data.apiCertificatePfx = cert.getString("CertyfikatPfx") + data.apiCertificateExpiryTime = 1598832000 + data.apiToken = data.apiToken?.substring(0, 3) + data.apiCertificatePrivate = getPrivateKeyFromCert( + if (data.apiToken?.get(0) == 'F') VULCAN_API_PASSWORD_FAKELOG else VULCAN_API_PASSWORD, + data.apiCertificatePfx ?: "" + ) + data.loginStore.removeLoginData("certificatePfx") + data.loginStore.removeLoginData("devicePin") + onSuccess() + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } + } + + Request.builder() + .url("${data.apiUrl}/$VULCAN_API_ENDPOINT_CERTIFICATE") + .userAgent(VULCAN_API_USER_AGENT) + .addHeader("RequestMobileType", "RegisterDevice") + .addParameter("PIN", data.apiPin) + .addParameter("TokenKey", data.apiToken) + .addParameter("DeviceId", UUID.randomUUID().toString()) + .addParameter("DeviceName", VULCAN_API_DEVICE_NAME) + .addParameter("DeviceNameUser", "") + .addParameter("DeviceDescription", "") + .addParameter("DeviceSystemType", "Android") + .addParameter("DeviceSystemVersion", Build.VERSION.RELEASE) + .addParameter("RemoteMobileTimeKey", currentTimeUnix()) + .addParameter("TimeKey", currentTimeUnix() - 1) + .addParameter("RequestId", UUID.randomUUID().toString()) + .addParameter("AppVersion", VULCAN_API_APP_VERSION) + .addParameter("RemoteMobileAppVersion", VULCAN_API_APP_VERSION) + .addParameter("RemoteMobileAppName", VULCAN_API_APP_NAME) + .postJson() + .allowErrorCode(HTTP_BAD_REQUEST) + .callback(callback) + .build() + .enqueue() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/AnnouncementGetEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/AnnouncementGetEvent.kt new file mode 100644 index 00000000..9278844a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/AnnouncementGetEvent.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-26 + */ + +package pl.szczodrzynski.edziennik.data.api.events + +import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull + +data class AnnouncementGetEvent(val announcement: AnnouncementFull) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskAllFinishedEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskAllFinishedEvent.kt new file mode 100644 index 00000000..a5d026cb --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskAllFinishedEvent.kt @@ -0,0 +1,7 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-28. + */ + +package pl.szczodrzynski.edziennik.data.api.events + +class ApiTaskAllFinishedEvent diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskErrorEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskErrorEvent.kt new file mode 100644 index 00000000..11c1813f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskErrorEvent.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-28. + */ + +package pl.szczodrzynski.edziennik.data.api.events + +import pl.szczodrzynski.edziennik.data.api.models.ApiError + +class ApiTaskErrorEvent(val error: ApiError) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskFinishedEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskFinishedEvent.kt new file mode 100644 index 00000000..7dba791d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskFinishedEvent.kt @@ -0,0 +1,7 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-28. + */ + +package pl.szczodrzynski.edziennik.data.api.events + +class ApiTaskFinishedEvent(val profileId: Int) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskProgressEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskProgressEvent.kt new file mode 100644 index 00000000..cc7463dc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskProgressEvent.kt @@ -0,0 +1,7 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-28. + */ + +package pl.szczodrzynski.edziennik.data.api.events + +class ApiTaskProgressEvent(val profileId: Int, val progress: Float, val progressText: String?) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskStartedEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskStartedEvent.kt new file mode 100644 index 00000000..20783e4b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/ApiTaskStartedEvent.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-28. + */ + +package pl.szczodrzynski.edziennik.data.api.events + +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile + +class ApiTaskStartedEvent(val profileId: Int, val profile: Profile? = null) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/AttachmentGetEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/AttachmentGetEvent.kt new file mode 100644 index 00000000..cbc85b7d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/AttachmentGetEvent.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-11-24 + */ + +package pl.szczodrzynski.edziennik.data.api.events + +data class AttachmentGetEvent(val profileId: Int, val messageId: Long, val attachmentId: Long, + var eventType: Int = TYPE_PROGRESS, val fileName: String? = null, + val bytesWritten: Long = 0) { + companion object { + const val TYPE_PROGRESS = 0 + const val TYPE_FINISHED = 1 + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/FirstLoginFinishedEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/FirstLoginFinishedEvent.kt new file mode 100644 index 00000000..b74ef2c7 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/FirstLoginFinishedEvent.kt @@ -0,0 +1,6 @@ +package pl.szczodrzynski.edziennik.data.api.events + +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile + +data class FirstLoginFinishedEvent(val profileList: List, val loginStore: LoginStore) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/MessageGetEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/MessageGetEvent.kt new file mode 100644 index 00000000..b9d72295 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/MessageGetEvent.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-12. + */ + +package pl.szczodrzynski.edziennik.data.api.events + +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull + +data class MessageGetEvent(val message: MessageFull) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/MessageSentEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/MessageSentEvent.kt new file mode 100644 index 00000000..f423ba4f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/MessageSentEvent.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-27. + */ + +package pl.szczodrzynski.edziennik.data.api.events + +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message + +data class MessageSentEvent(val profileId: Int, val message: Message?, val sentDate: Long?) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/RecipientListGetEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/RecipientListGetEvent.kt new file mode 100644 index 00000000..60754de3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/RecipientListGetEvent.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-22. + */ + +package pl.szczodrzynski.edziennik.data.api.events + +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher + +data class RecipientListGetEvent(val profileId: Int, val teacherList: List) \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/requests/ServiceCloseRequest.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/requests/ServiceCloseRequest.kt new file mode 100644 index 00000000..a4a696a4 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/requests/ServiceCloseRequest.kt @@ -0,0 +1,7 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-1. + */ + +package pl.szczodrzynski.edziennik.data.api.events.requests + +class ServiceCloseRequest diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/requests/TaskCancelRequest.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/requests/TaskCancelRequest.kt new file mode 100644 index 00000000..2985daac --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/requests/TaskCancelRequest.kt @@ -0,0 +1,7 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-1. + */ + +package pl.szczodrzynski.edziennik.data.api.events.requests + +class TaskCancelRequest(val taskId: Int) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/AttachmentGetCallback.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/AttachmentGetCallback.java deleted file mode 100644 index 0bd76455..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/AttachmentGetCallback.java +++ /dev/null @@ -1,11 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api.interfaces; - -import im.wangchao.mhttp.Request; - -/** - * Callback containing a {@link Request.Builder} which has correct headers and body to download a corresponding message attachment when ran. - * {@code onSuccess} has to be ran on the UI thread. - */ -public interface AttachmentGetCallback { - void onSuccess(Request.Builder builder); -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt new file mode 100644 index 00000000..c1c878b4 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-29. + */ + +package pl.szczodrzynski.edziennik.data.api.interfaces + +import pl.szczodrzynski.edziennik.data.api.models.Feature +import pl.szczodrzynski.edziennik.data.api.models.LoginMethod + +/** + * A callback passed only to an e-register class. + * All [Feature]s and [LoginMethod]s receive this callback, + * but may only use [EndpointCallback]'s methods. + */ +interface EdziennikCallback : EndpointCallback { + fun onCompleted() +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikInterface.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikInterface.java deleted file mode 100644 index 962b1bef..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikInterface.java +++ /dev/null @@ -1,92 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api.interfaces; - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Map; - -import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore; -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull; -import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher; -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesComposeInfo; -import pl.szczodrzynski.edziennik.utils.models.Endpoint; - -public interface EdziennikInterface { - - /** - * Sync all Edziennik data. - * Ran always on worker thread. - * - * @param activityContext a {@link Context}, used for resource extractions, passed back to {@link SyncCallback} - * @param callback ran on worker thread. - * @param profileId - * @param profile - * @param loginStore - */ - void sync(@NonNull Context activityContext, @NonNull SyncCallback callback, int profileId, @Nullable Profile profile, @NonNull LoginStore loginStore); - void syncMessages(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile); - void syncFeature(@NonNull Context activityContext, @NonNull SyncCallback callback, @NonNull ProfileFull profile, int ... featureList); - - int FEATURE_ALL = 0; - int FEATURE_TIMETABLE = 1; - int FEATURE_AGENDA = 2; - int FEATURE_GRADES = 3; - int FEATURE_HOMEWORK = 4; - int FEATURE_NOTICES = 5; - int FEATURE_ATTENDANCE = 6; - int FEATURE_MESSAGES_INBOX = 7; - int FEATURE_MESSAGES_OUTBOX = 8; - int FEATURE_ANNOUNCEMENTS = 9; - - /** - * Download a single message or get its recipient list if it's already downloaded. - * - * May be executed on any thread. - * - * @param activityContext - * @param errorCallback used for error reporting. Ran on a background thread. - * @param profile - * @param message a message of which body and recipient list should be downloaded. - * @param messageCallback always executed on UI thread. - */ - void getMessage(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull MessageFull message, @NonNull MessageGetCallback messageCallback); - void getAttachment(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull MessageFull message, long attachmentId, @NonNull AttachmentGetCallback attachmentCallback); - //void getMessageList(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, int type, @NonNull MessageListCallback messageCallback); - /** - * Download a list of available message recipients. - * - * Updates a database-saved {@code teacherList} with {@code loginId}s. - * - * A {@link Teacher} is considered as a recipient when its {@code loginId} is not null. - * - * May be executed on any thread. - * - * @param activityContext - * @param errorCallback used for error reporting. Ran on a background thread. - * @param profile - * @param recipientListGetCallback always executed on UI thread. - */ - void getRecipientList(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull RecipientListGetCallback recipientListGetCallback); - MessagesComposeInfo getComposeInfo(@NonNull ProfileFull profile); - - - /** - * - * @param profile a {@link Profile} containing already changed endpoints - * @return a map of configurable {@link Endpoint}s along with their names, {@code null} when unsupported - */ - Map getConfigurableEndpoints(Profile profile); - - /** - * Check if the specified endpoint is enabled for the current profile. - * - * @param profile a {@link Profile} containing already changed endpoints - * @param defaultActive if the endpoint is enabled by default. - * @param name the endpoint's name - * @return {@code true} if the endpoint is enabled, {@code false} when it's not. Return {@code defaultActive} if unsupported. - */ - boolean isEndpointEnabled(Profile profile, boolean defaultActive, String name); -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikInterface.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikInterface.kt new file mode 100644 index 00000000..5c4bfdc2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikInterface.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-29. + */ + +package pl.szczodrzynski.edziennik.data.api.interfaces + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher + +interface EdziennikInterface { + fun sync(featureIds: List, viewId: Int? = null, arguments: JsonObject? = null) + fun getMessage(message: MessageFull) + fun sendMessage(recipients: List, subject: String, text: String) + fun markAllAnnouncementsAsRead() + fun getAnnouncement(announcement: AnnouncementFull) + fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) + fun getRecipientList() + fun firstLogin() + fun cancel() +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EndpointCallback.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EndpointCallback.kt new file mode 100644 index 00000000..b44820c5 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EndpointCallback.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-29. + */ + +package pl.szczodrzynski.edziennik.data.api.interfaces + +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.models.Feature +import pl.szczodrzynski.edziennik.data.api.models.LoginMethod + +/** + * A callback passed to all [Feature]s and [LoginMethod]s + */ +interface EndpointCallback { + fun onError(apiError: ApiError) + fun onProgress(step: Float) + fun onStartProgress(stringRes: Int) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/ErrorCallback.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/ErrorCallback.java deleted file mode 100644 index cc8f2375..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/ErrorCallback.java +++ /dev/null @@ -1,11 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api.interfaces; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import pl.szczodrzynski.edziennik.data.api.AppError; - -public interface ErrorCallback { - void onError(Context activityContext, @NonNull AppError error); -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/LoginCallback.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/LoginCallback.java deleted file mode 100644 index cae0c65e..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/LoginCallback.java +++ /dev/null @@ -1,5 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api.interfaces; - -public interface LoginCallback { - void onSuccess(); -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/MessageGetCallback.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/MessageGetCallback.java deleted file mode 100644 index 7fd973c8..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/MessageGetCallback.java +++ /dev/null @@ -1,11 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api.interfaces; - -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull; - -/** - * Callback containing a {@link MessageFull} which already has its {@code body} and {@code recipients}. - * {@code onSuccess} is always ran on the UI thread. - */ -public interface MessageGetCallback { - void onSuccess(MessageFull message); -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/MessageListCallback.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/MessageListCallback.java deleted file mode 100644 index 71561ffd..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/MessageListCallback.java +++ /dev/null @@ -1,9 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api.interfaces; - -import java.util.List; - -import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull; - -public interface MessageListCallback { - void onSuccess(List messageList); -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/ProgressCallback.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/ProgressCallback.java deleted file mode 100644 index 32948eb9..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/ProgressCallback.java +++ /dev/null @@ -1,8 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api.interfaces; - -import androidx.annotation.StringRes; - -public interface ProgressCallback extends ErrorCallback { - void onProgress(int progressStep); - void onActionStarted(@StringRes int stringResId); -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/RecipientListGetCallback.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/RecipientListGetCallback.java deleted file mode 100644 index fc249432..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/RecipientListGetCallback.java +++ /dev/null @@ -1,9 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api.interfaces; - -import java.util.List; - -import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher; - -public interface RecipientListGetCallback { - void onSuccess(List teacherList); -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/SyncCallback.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/SyncCallback.java deleted file mode 100644 index dfaf243b..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/SyncCallback.java +++ /dev/null @@ -1,19 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api.interfaces; - -import android.content.Context; - -import java.util.List; - -import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile; -import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull; - -/** - * A callback used for error reporting, progress information. - * All the methods are always ran on a worker thread. - */ -public interface SyncCallback extends ProgressCallback { - void onLoginFirst(List profileList, LoginStore loginStore); - void onSuccess(Context activityContext, ProfileFull profileFull); - -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt new file mode 100644 index 00000000..f9c553a4 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-28. + */ + +package pl.szczodrzynski.edziennik.data.api.models + +import android.content.Context +import com.google.gson.JsonObject +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.szkolny.request.ErrorReportRequest +import pl.szczodrzynski.edziennik.stackTraceString + +class ApiError(val tag: String, var errorCode: Int) { + val id = System.currentTimeMillis() + var profileId: Int? = null + var throwable: Throwable? = null + var apiResponse: String? = null + var request: Request? = null + var response: Response? = null + var isCritical = true + + fun withThrowable(throwable: Throwable?): ApiError { + this.throwable = throwable + return this + } + fun withApiResponse(apiResponse: String?): ApiError { + this.apiResponse = apiResponse + return this + } + fun withApiResponse(apiResponse: JsonObject?): ApiError { + this.apiResponse = apiResponse?.toString() + return this + } + fun withRequest(request: Request?): ApiError { + this.request = request + return this + } + fun withResponse(response: Response?): ApiError { + this.response = response + this.request = response?.request() + return this + } + + fun setCritical(isCritical: Boolean): ApiError { + this.isCritical = isCritical + return this + } + + fun getStringText(context: Context): String { + return context.resources.getIdentifier("error_${errorCode}", "string", context.packageName).let { + if (it != 0) + context.getString(it) + else + "?" + } + } + + fun getStringReason(context: Context): String { + return context.resources.getIdentifier("error_${errorCode}_reason", "string", context.packageName).let { + if (it != 0) + context.getString(it) + else + context.getString(R.string.error_unknown_format, errorCode, tag) + } + } + + override fun toString(): String { + return "ApiError(tag='$tag', errorCode=$errorCode, profileId=$profileId, throwable=$throwable, apiResponse=$apiResponse, request=$request, response=$response, isCritical=$isCritical)" + } + + fun toReportableError(context: Context): ErrorReportRequest.Error { + val requestString = request?.let { + it.method() + " " + it.url() + "\n" + it.headers() + } + val responseString = response?.let { + if (it.parserErrorBody == null) { + try { + it.parserErrorBody = it.raw().body()?.string() + } catch (e: Exception) { + it.parserErrorBody = e.stackTraceString + } + } + "HTTP "+it.code()+" "+it.message()+"\n" + it.headers() + "\n\n" + it.parserErrorBody + } + return ErrorReportRequest.Error( + id = id, + tag = tag, + errorCode = errorCode, + errorText = getStringText(context), + errorReason = getStringReason(context), + stackTrace = throwable?.stackTraceString, + request = requestString, + response = responseString, + apiResponse = apiResponse, + isCritical = isCritical + ) + } + +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt new file mode 100644 index 00000000..6fb1af98 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt @@ -0,0 +1,437 @@ +package pl.szczodrzynski.edziennik.data.api.models + +import android.util.LongSparseArray +import android.util.SparseArray +import androidx.core.util.size +import androidx.room.OnConflictStrategy +import com.google.gson.JsonObject +import im.wangchao.mhttp.Response +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.interfaces.EndpointCallback +import pl.szczodrzynski.edziennik.data.db.AppDb +import pl.szczodrzynski.edziennik.data.db.modules.announcements.Announcement +import pl.szczodrzynski.edziennik.data.db.modules.api.EndpointTimer +import pl.szczodrzynski.edziennik.data.db.modules.attendance.Attendance +import pl.szczodrzynski.edziennik.data.db.modules.attendance.AttendanceType +import pl.szczodrzynski.edziennik.data.db.modules.classrooms.Classroom +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.events.EventType +import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory +import pl.szczodrzynski.edziennik.data.db.modules.lessons.Lesson +import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange +import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonRange +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.luckynumber.LuckyNumber +import pl.szczodrzynski.edziennik.data.db.modules.messages.Message +import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.notices.Notice +import pl.szczodrzynski.edziennik.data.db.modules.notices.NoticeType +import pl.szczodrzynski.edziennik.data.db.modules.notification.Notification +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject +import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher +import pl.szczodrzynski.edziennik.data.db.modules.teachers.TeacherAbsence +import pl.szczodrzynski.edziennik.data.db.modules.teachers.TeacherAbsenceType +import pl.szczodrzynski.edziennik.data.db.modules.teams.Team +import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.toSparseArray +import pl.szczodrzynski.edziennik.utils.Utils.d +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.values +import java.io.InterruptedIOException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import javax.net.ssl.SSLException + +open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore) { + companion object { + private const val TAG = "Data" + } + + var fakeLogin = false + + var cancelled = false + + val profileId + get() = profile?.id ?: -1 + + var arguments: JsonObject? = null + + /** + * A callback passed to all [Feature]s and [LoginMethod]s + */ + lateinit var callback: EndpointCallback + + /** + * A list of [LoginMethod]s *already fulfilled* during this sync. + * + * A [LoginMethod] may add elements to this list only after a successful login + * with that method. + */ + val loginMethods = mutableListOf() + + /** + * A method which may be overridden in child Data* classes. + * + * Calling it should populate [loginMethods] with all + * already available login methods (e.g. a non-expired OAuth token). + */ + open fun satisfyLoginMethods() {} + + /** + * A list of Login method IDs that are still pending + * to run. + */ + var targetLoginMethodIds = mutableListOf() + /** + * A list of endpoint IDs that are still pending + * to run. + */ + var targetEndpointIds = mutableListOf() + /** + * A count of all network requests to do. + */ + var progressCount: Int = 0 + /** + * A number by which the progress will be incremented, every time + * a login method/endpoint finishes its job. + */ + var progressStep: Float = 0f + + /** + * A map of endpoint IDs to JSON objects, specifying their arguments bundle. + */ + var endpointArgs = mutableMapOf() + + var endpointTimers = mutableListOf() + + val notifications = mutableListOf() + + val teacherList = LongSparseArray() + val subjectList = LongSparseArray() + val teamList = LongSparseArray() + val lessonRanges = SparseArray() + val gradeCategories = LongSparseArray() + + var teacherOnConflictStrategy = OnConflictStrategy.IGNORE + + val classrooms = LongSparseArray() + val attendanceTypes = LongSparseArray() + val noticeTypes = LongSparseArray() + val eventTypes = LongSparseArray() + val teacherAbsenceTypes = LongSparseArray() + + private var mTeamClass: Team? = null + var teamClass: Team? + get() { + if (mTeamClass == null) + mTeamClass = teamList.singleOrNull { it.type == Team.TYPE_CLASS } + return mTeamClass + } + set(value) { + mTeamClass = value + } + + var toRemove = mutableListOf() + + val lessonList = mutableListOf() + val lessonChangeList = mutableListOf() + val lessonNewList = mutableListOf() + + val gradeList = mutableListOf() + + val eventList = mutableListOf() + + val noticeList = mutableListOf() + + val attendanceList = mutableListOf() + + val announcementList = mutableListOf() + val announcementIgnoreList = mutableListOf() + + val luckyNumberList = mutableListOf() + + val teacherAbsenceList = mutableListOf() + + val messageList = mutableListOf() + val messageIgnoreList = mutableListOf() + val messageRecipientList = mutableListOf() + val messageRecipientIgnoreList = mutableListOf() + + val metadataList = mutableListOf() + val setSeenMetadataList = mutableListOf() + + val db: AppDb by lazy { app.db } + + init { + if (App.devMode) { + fakeLogin = loginStore.hasLoginData("fakeLogin") + } + clear() + if (profile != null) { + endpointTimers = db.endpointTimerDao().getAllNow(profile.id).toMutableList() + db.teacherDao().getAllNow(profileId).toSparseArray(teacherList) { it.id } + db.subjectDao().getAllNow(profileId).toSparseArray(subjectList) { it.id } + db.teamDao().getAllNow(profileId).toSparseArray(teamList) { it.id } + db.lessonRangeDao().getAllNow(profileId).toSparseArray(lessonRanges) { it.lessonNumber } + db.gradeCategoryDao().getAllNow(profileId).toSparseArray(gradeCategories) { it.categoryId } + } + } + + fun clear() { + loginMethods.clear() + + toRemove.clear() + endpointTimers.clear() + teacherList.clear() + subjectList.clear() + teamList.clear() + lessonRanges.clear() + gradeCategories.clear() + + classrooms.clear() + attendanceTypes.clear() + noticeTypes.clear() + eventTypes.clear() + teacherAbsenceTypes.clear() + + lessonList.clear() + lessonChangeList.clear() + lessonNewList.clear() + gradeList.clear() + noticeList.clear() + attendanceList.clear() + announcementList.clear() + announcementIgnoreList.clear() + luckyNumberList.clear() + teacherAbsenceList.clear() + messageList.clear() + messageIgnoreList.clear() + messageRecipientList.clear() + messageRecipientIgnoreList.clear() + metadataList.clear() + setSeenMetadataList.clear() + } + + open fun saveData() { + if (profile == null) + return // return on first login + + profile.empty = false + + db.profileDao().add(profile) + db.loginStoreDao().add(loginStore) + + if (profile.id == app.profile?.id) { + app.profile.apply { + name = profile.name + subname = profile.subname + syncEnabled = profile.syncEnabled + loggedIn = profile.loggedIn + empty = profile.empty + archived = profile.archived + studentNameLong = profile.studentNameLong + studentNameShort = profile.studentNameShort + studentNumber = profile.studentNumber + studentData = profile.studentData + accountNameLong = profile.accountNameLong + yearAverageMode = profile.yearAverageMode + currentSemester = profile.currentSemester + attendancePercentage = profile.attendancePercentage + dateSemester1Start = profile.dateSemester1Start + dateSemester2Start = profile.dateSemester2Start + dateYearEnd = profile.dateYearEnd + luckyNumberEnabled = profile.luckyNumberEnabled + luckyNumber = profile.luckyNumber + luckyNumberDate = profile.luckyNumberDate + lastFullSync = profile.lastFullSync + lastReceiversSync = profile.lastReceiversSync + } + } + if (loginStore.id == app.profile?.loginStoreId) { + app.loginStore = loginStore.data + app.profile.loginStoreData = loginStore.data + } + + // always present and not empty, during every sync + db.endpointTimerDao().addAll(endpointTimers) + if (teacherOnConflictStrategy == OnConflictStrategy.IGNORE) + db.teacherDao().addAllIgnore(teacherList.values()) + else if (teacherOnConflictStrategy == OnConflictStrategy.REPLACE) + db.teacherDao().addAll(teacherList.values()) + db.subjectDao().addAll(subjectList.values()) + db.teamDao().clear(profileId) + db.teamDao().addAll(teamList.values()) + db.lessonRangeDao().clear(profileId) + db.lessonRangeDao().addAll(lessonRanges.values()) + db.gradeCategoryDao().clear(profileId) + db.gradeCategoryDao().addAll(gradeCategories.values()) + + // may be empty - extracted from DB on demand, by an endpoint + if (classrooms.size > 0) + db.classroomDao().addAll(classrooms.values()) + if (attendanceTypes.size > 0) + db.attendanceTypeDao().addAll(attendanceTypes.values()) + if (noticeTypes.size > 0) + db.noticeTypeDao().addAll(noticeTypes.values()) + if (eventTypes.size > 0) + db.eventTypeDao().addAll(eventTypes.values()) + if (teacherAbsenceTypes.size > 0) + db.teacherAbsenceTypeDao().addAll(teacherAbsenceTypes.values()) + + // clear DB with DataRemoveModels added by endpoints + for (model in toRemove) { + when (model) { + is DataRemoveModel.Timetable -> model.commit(profileId, db.timetableDao()) + is DataRemoveModel.Grades -> model.commit(profileId, db.gradeDao()) + is DataRemoveModel.Events -> model.commit(profileId, db.eventDao()) + } + } + + if (metadataList.isNotEmpty()) + db.metadataDao().addAllIgnore(metadataList) + if (setSeenMetadataList.isNotEmpty()) + db.metadataDao().setSeen(setSeenMetadataList) + + // not extracted from DB - always new data + if (lessonList.isNotEmpty()) { + db.lessonDao().clear(profile.id) + db.lessonDao().addAll(lessonList) + } + if (lessonChangeList.isNotEmpty()) + db.lessonChangeDao().addAll(lessonChangeList) + if (lessonNewList.isNotEmpty()) { + db.timetableDao() += lessonNewList + } + if (gradeList.isNotEmpty()) { + db.gradeDao().addAll(gradeList) + } + if (eventList.isNotEmpty()) { + db.eventDao().addAll(eventList) + } + if (noticeList.isNotEmpty()) { + db.noticeDao().clear(profile.id) + db.noticeDao().addAll(noticeList) + } + if (attendanceList.isNotEmpty()) + db.attendanceDao().addAll(attendanceList) + if (announcementList.isNotEmpty()) + db.announcementDao().addAll(announcementList) + if (announcementIgnoreList.isNotEmpty()) + db.announcementDao().addAllIgnore(announcementIgnoreList) + if (luckyNumberList.isNotEmpty()) + db.luckyNumberDao().addAll(luckyNumberList) + if (teacherAbsenceList.isNotEmpty()) + db.teacherAbsenceDao().addAll(teacherAbsenceList) + + if (messageList.isNotEmpty()) + db.messageDao().addAll(messageList) + if (messageIgnoreList.isNotEmpty()) + db.messageDao().addAllIgnore(messageIgnoreList) + if (messageRecipientList.isNotEmpty()) + db.messageRecipientDao().addAll(messageRecipientList) + if (messageRecipientIgnoreList.isNotEmpty()) + db.messageRecipientDao().addAllIgnore(messageRecipientIgnoreList) + } + + fun notify(onSuccess: () -> Unit) { + if (profile == null) { + onSuccess() + return + } + try { + DataNotifications(this) + db.notificationDao().addAll(notifications) + onSuccess() + } catch (e: Exception) { + error(ApiError(TAG, EXCEPTION_NOTIFY) + .withThrowable(e)) + } + } + + fun setSyncNext(endpointId: Int, syncIn: Long? = null, viewId: Int? = null, syncAt: Long? = null) { + EndpointTimer(profile?.id ?: -1, endpointId).apply { + syncedNow() + + if (syncIn != null) { + if (syncIn < 10) + nextSync = syncIn + else + syncIn(syncIn) + } + if (syncAt != null) { + nextSync = syncAt + } + if (viewId != null) + syncWhenView(viewId) + + endpointTimers.add(this) + } + } + + fun cancel() { + d("Data", "Cancelled") + cancelled = true + saveData() + } + + fun shouldSyncLuckyNumber(): Boolean { + return (db.luckyNumberDao().getNearestFutureNow(profileId, Date.getToday().value) ?: -1) == -1 + } + + /*fun error(tag: String, errorCode: Int, response: Response? = null, throwable: Throwable? = null, apiResponse: JsonObject? = null) { + val code = when (throwable) { + is UnknownHostException, is SSLException, is InterruptedIOException -> CODE_NO_INTERNET + is SocketTimeoutException -> CODE_TIMEOUT + else -> when (response?.code()) { + 400, 401, 424, 500, 503, 404 -> CODE_MAINTENANCE + else -> errorCode + } + } + error(ApiError(tag, code).apply { + profileId = profile?.id ?: -1 + }.withResponse(response).withThrowable(throwable).withApiResponse(apiResponse)) + }*/ + + fun error(tag: String, errorCode: Int, response: Response? = null, apiResponse: String? = null) { + error(ApiError(tag, errorCode).apply { + profileId = profile?.id ?: -1 + }.withResponse(response).withApiResponse(apiResponse)) + } + + fun error(apiError: ApiError) { + apiError.errorCode = when (apiError.throwable) { + is UnknownHostException -> ERROR_REQUEST_FAILURE_HOSTNAME_NOT_FOUND + is SSLException -> ERROR_REQUEST_FAILURE_SSL_ERROR + is InterruptedIOException -> ERROR_REQUEST_FAILURE_NO_INTERNET + is SocketTimeoutException -> ERROR_REQUEST_FAILURE_TIMEOUT + else -> + if (apiError.errorCode == ERROR_REQUEST_FAILURE) + when (apiError.response?.code()) { + 400 -> ERROR_REQUEST_HTTP_400 + 401 -> ERROR_REQUEST_HTTP_401 + 403 -> ERROR_REQUEST_HTTP_403 + 404 -> ERROR_REQUEST_HTTP_404 + 405 -> ERROR_REQUEST_HTTP_405 + 410 -> ERROR_REQUEST_HTTP_410 + 424 -> ERROR_REQUEST_HTTP_424 + 500 -> ERROR_REQUEST_HTTP_500 + 503 -> ERROR_REQUEST_HTTP_503 + else -> apiError.errorCode + } + else apiError.errorCode + } + callback.onError(apiError) + } + + fun progress(step: Float) { + callback.onProgress(step) + } + + fun startProgress(stringRes: Int) { + callback.onStartProgress(stringRes) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/DataRemoveModel.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/DataRemoveModel.kt new file mode 100644 index 00000000..0b8f1265 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/DataRemoveModel.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-10-2. + */ + +package pl.szczodrzynski.edziennik.data.api.models + +import pl.szczodrzynski.edziennik.data.db.modules.events.EventDao +import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeDao +import pl.szczodrzynski.edziennik.data.db.modules.timetable.TimetableDao +import pl.szczodrzynski.edziennik.utils.models.Date + +open class DataRemoveModel { + class Timetable(private val dateFrom: Date?, private val dateTo: Date?) : DataRemoveModel() { + companion object { + fun from(dateFrom: Date) = Timetable(dateFrom, null) + fun to(dateTo: Date) = Timetable(null, dateTo) + fun between(dateFrom: Date, dateTo: Date) = Timetable(dateFrom, dateTo) + } + + fun commit(profileId: Int, dao: TimetableDao) { + if (dateFrom != null && dateTo != null) { + dao.clearBetweenDates(profileId, dateFrom, dateTo) + } else { + dateFrom?.let { dateFrom -> dao.clearFromDate(profileId, dateFrom) } + dateTo?.let { dateTo -> dao.clearToDate(profileId, dateTo) } + } + } + } + + class Grades(private val all: Boolean, private val semester: Int?, private val type: Int?) : DataRemoveModel() { + companion object { + fun all() = Grades(true, null, null) + fun allWithType(type: Int) = Grades(true, null, type) + fun semester(semester: Int) = Grades(false, semester, null) + fun semesterWithType(semester: Int, type: Int) = Grades(false, semester, type) + } + + fun commit(profileId: Int, dao: GradeDao) { + if (all) { + if (type != null) dao.clearWithType(profileId, type) + else dao.clear(profileId) + } + semester?.let { + if (type != null) dao.clearForSemesterWithType(profileId, it, type) + else dao.clearForSemester(profileId, it) + } + } + } + + class Events(private val type: Int?, private val exceptType: Int?, private val exceptTypes: List?) : DataRemoveModel() { + companion object { + fun futureExceptType(exceptType: Int) = Events(null, exceptType, null) + fun futureExceptTypes(exceptTypes: List) = Events(null, null, exceptTypes) + fun futureWithType(type: Int) = Events(type, null, null) + } + + fun commit(profileId: Int, dao: EventDao) { + type?.let { dao.removeFutureWithType(profileId, Date.getToday(), it) } + exceptType?.let { dao.removeFutureExceptType(profileId, Date.getToday(), it) } + exceptTypes?.let { dao.removeFutureExceptTypes(profileId, Date.getToday(), it) } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Feature.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Feature.kt new file mode 100644 index 00000000..fd7c7873 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Feature.kt @@ -0,0 +1,33 @@ +package pl.szczodrzynski.edziennik.data.api.models + +/** + * A Endpoint descriptor class. + * + * The API runs appropriate endpoints in order to fulfill its + * feature list. + * An endpoint may have its [LoginMethod] dependencies which will be + * satisfied by the API before the [endpointClass]'s constructor is invoked. + * + * @param loginType type of the e-register this endpoint handles + * @param featureId a feature ID + * @param endpointIds a [List] of [Feature]s that satisfy this feature ID + * @param requiredLoginMethod a required login method, which will have to be executed before this endpoint. + */ +data class Feature( + val loginType: Int, + val featureId: Int, + val endpointIds: List>, + val requiredLoginMethods: List +) { + var priority = endpointIds.size + fun withPriority(priority: Int): Feature { + this.priority = priority + return this + } + + var shouldSync: ((Data) -> Boolean)? = null + fun withShouldSync(shouldSync: ((Data) -> Boolean)?): Feature { + this.shouldSync = shouldSync + return this + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/LoginMethod.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/LoginMethod.kt new file mode 100644 index 00000000..e5efbd0c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/LoginMethod.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-9-20. + */ + +package pl.szczodrzynski.edziennik.data.api.models + +import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_NOT_NEEDED +import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile + +/** + * A Login Method descriptor class. + * + * This is used by the API to satisfy all [Feature]s' dependencies. + * A login method may have its own dependencies which need to be + * satisfied before the [loginMethodClass]'s constructor is invoked. + * + * @param loginType type of the e-register this login method handles + * @param loginMethodId a unique ID of this login method + * @param loginMethodClass a [Class] which constructor will be invoked when a log in is needed + * @param requiredLoginMethod a required login method (which will be called before this). May differ depending on the [Profile] and/or [LoginStore]. + */ +class LoginMethod( + val loginType: Int, + val loginMethodId: Int, + val loginMethodClass: Class<*>, + private var mIsPossible: ((profile: Profile?, loginStore: LoginStore) -> Boolean)? = null, + private var mRequiredLoginMethod: ((profile: Profile?, loginStore: LoginStore) -> Int)? = null +) { + + fun withIsPossible(isPossible: (profile: Profile?, loginStore: LoginStore) -> Boolean): LoginMethod { + this.mIsPossible = isPossible + return this + } + fun withRequiredLoginMethod(requiredLoginMethod: (profile: Profile?, loginStore: LoginStore) -> Int): LoginMethod { + this.mRequiredLoginMethod = requiredLoginMethod + return this + } + + fun isPossible(profile: Profile?, loginStore: LoginStore): Boolean { + return mIsPossible?.invoke(profile, loginStore) ?: false + } + fun requiredLoginMethod(profile: Profile?, loginStore: LoginStore): Int { + return mRequiredLoginMethod?.invoke(profile, loginStore) ?: LOGIN_METHOD_NOT_NEEDED + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/Szkolny.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/Szkolny.kt new file mode 100644 index 00000000..071f6be1 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/Szkolny.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-13 + */ + +package pl.szczodrzynski.edziennik.data.api.szkolny + +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull + +class Szkolny(val app: App, val callback: EdziennikCallback) { + + private val api = SzkolnyApi(app) + + fun sync(profileList: List) { + val profiles = profileList.filter { it.registration == Profile.REGISTRATION_ENABLED } + if (profiles.isNotEmpty()) { + val events = api.getEvents(profiles) + + if (events.isNotEmpty()) { + app.db.eventDao().addAll(events) + app.db.metadataDao().addAllIgnore(events.map { event -> + Metadata( + event.profileId, + Metadata.TYPE_EVENT, + event.id, + event.seen, + event.notified, + event.addedDate + ) + }) + } + } + + completed() + } + + /*fun shareEvent(event: EventFull) { + api.shareEvent(event) + completed() + } + + fun unshareEvent(event: EventFull) { + api.unshareEvent(event) + completed() + }*/ + + private fun completed() { + callback.onCompleted() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt new file mode 100644 index 00000000..f5521520 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt @@ -0,0 +1,175 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-8 + */ + +package pl.szczodrzynski.edziennik.data.api.szkolny + +import android.os.Build +import com.google.gson.GsonBuilder +import okhttp3.OkHttpClient +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter +import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.TimeAdapter +import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor +import pl.szczodrzynski.edziennik.data.api.szkolny.request.ErrorReportRequest +import pl.szczodrzynski.edziennik.data.api.szkolny.request.EventShareRequest +import pl.szczodrzynski.edziennik.data.api.szkolny.request.ServerSyncRequest +import pl.szczodrzynski.edziennik.data.api.szkolny.request.WebPushRequest +import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse +import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse +import pl.szczodrzynski.edziennik.data.db.modules.events.Event +import pl.szczodrzynski.edziennik.data.db.modules.events.EventFull +import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.create +import java.util.concurrent.TimeUnit.SECONDS + +class SzkolnyApi(val app: App) { + + private val api: SzkolnyService + + init { + val okHttpClient: OkHttpClient = app.http.newBuilder() + .followRedirects(true) + .callTimeout(30, SECONDS) + .addInterceptor(SignatureInterceptor(app)) + .build() + + val gsonConverterFactory = GsonConverterFactory.create( + GsonBuilder() + .setLenient() + .registerTypeAdapter(Date::class.java, DateAdapter()) + .registerTypeAdapter(Time::class.java, TimeAdapter()) + .create()) + + val retrofit: Retrofit = Retrofit.Builder() + .baseUrl("https://api.szkolny.eu/") + .addConverterFactory(gsonConverterFactory) + .client(okHttpClient) + .build() + + api = retrofit.create() + } + + fun getEvents(profiles: List): List { + val teams = app.db.teamDao().allNow + val notifications = app.db.notificationDao().getNotPostedNow() + + val response = api.serverSync(ServerSyncRequest( + deviceId = app.deviceId, + device = ServerSyncRequest.Device( + osType = "Android", + osVersion = Build.VERSION.RELEASE, + hardware = "${Build.MANUFACTURER} ${Build.MODEL}", + pushToken = app.config.sync.tokenApp, + appVersion = BuildConfig.VERSION_NAME, + appType = BuildConfig.BUILD_TYPE, + appVersionCode = BuildConfig.VERSION_CODE, + syncInterval = app.config.sync.interval + ), + userCodes = profiles.map { it.usernameId }, + users = profiles.map { profile -> + ServerSyncRequest.User( + profile.usernameId, + profile.studentNameLong ?: "", + profile.studentNameShort ?: "", + profile.loginStoreType, + teams.filter { it.profileId == profile.id }.map { it.code } + ) + }, + notifications = notifications.map { ServerSyncRequest.Notification(it.profileName ?: "", it.type, it.text) } + )).execute().body() + + val events = mutableListOf() + + response?.data?.events?.forEach { event -> + teams.filter { it.code == event.teamCode }.forEach { team -> + val profile = profiles.firstOrNull { it.id == team.profileId } + + events.add(event.apply { + profileId = team.profileId + teamId = team.id + addedManually = true + seen = profile?.empty ?: false + notified = profile?.empty ?: false + + if (profile?.usernameId == event.sharedBy) sharedBy = "self" + }) + } + } + + return events + } + + fun shareEvent(event: EventFull): ApiResponse? { + val team = app.db.teamDao().getByIdNow(event.profileId, event.teamId) + + return api.shareEvent(EventShareRequest( + deviceId = app.deviceId, + sharedByName = event.sharedByName, + shareTeamCode = team.code, + event = event + )).execute().body() + } + + fun unshareEvent(event: Event): ApiResponse? { + val team = app.db.teamDao().getByIdNow(event.profileId, event.teamId) + + return api.shareEvent(EventShareRequest( + deviceId = app.deviceId, + sharedByName = event.sharedByName, + unshareTeamCode = team.code, + eventId = event.id + )).execute().body() + } + + /*fun eventEditRequest(requesterName: String, event: Event): ApiResponse? { + + }*/ + + fun pairBrowser(browserId: String?, pairToken: String?, onError: ((List) -> Unit)? = null): List { + val response = api.webPush(WebPushRequest( + action = "pairBrowser", + deviceId = app.deviceId, + browserId = browserId, + pairToken = pairToken + )).execute().body() + + response?.errors?.let { + onError?.invoke(it) + return emptyList() + } + + return response?.data?.browsers ?: emptyList() + } + + fun listBrowsers(onError: ((List) -> Unit)? = null): List { + val response = api.webPush(WebPushRequest( + action = "listBrowsers", + deviceId = app.deviceId + )).execute().body() + + return response?.data?.browsers ?: emptyList() + } + + fun unpairBrowser(browserId: String): List { + val response = api.webPush(WebPushRequest( + action = "unpairBrowser", + deviceId = app.deviceId, + browserId = browserId + )).execute().body() + + return response?.data?.browsers ?: emptyList() + } + + fun errorReport(errors: List): ApiResponse? { + return api.errorReport(ErrorReportRequest( + deviceId = app.deviceId, + errors = errors + )).execute().body() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt new file mode 100644 index 00000000..7d5f73e5 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-8 + */ + +package pl.szczodrzynski.edziennik.data.api.szkolny + +import pl.szczodrzynski.edziennik.data.api.szkolny.request.ErrorReportRequest +import pl.szczodrzynski.edziennik.data.api.szkolny.request.EventShareRequest +import pl.szczodrzynski.edziennik.data.api.szkolny.request.ServerSyncRequest +import pl.szczodrzynski.edziennik.data.api.szkolny.request.WebPushRequest +import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse +import pl.szczodrzynski.edziennik.data.api.szkolny.response.ServerSyncResponse +import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +interface SzkolnyService { + + @POST("appSync") + fun serverSync(@Body request: ServerSyncRequest): Call> + + @POST("share") + fun shareEvent(@Body request: EventShareRequest): Call> + + @POST("webPush") + fun webPush(@Body request: WebPushRequest): Call> + + @POST("errorReport") + fun errorReport(@Body request: ErrorReportRequest): Call> +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/adapter/DateAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/adapter/DateAdapter.kt new file mode 100644 index 00000000..01f2d6ef --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/adapter/DateAdapter.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-8 + */ + +package pl.szczodrzynski.edziennik.data.api.szkolny.adapter + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import pl.szczodrzynski.edziennik.utils.models.Date + +class DateAdapter : TypeAdapter() { + override fun write(writer: JsonWriter?, date: Date?) { + if (date == null) { + writer?.nullValue() + } else { + writer?.value(date.value) + } + } + + override fun read(reader: JsonReader?): Date? { + if (reader?.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + return reader?.nextInt()?.let { Date.fromValue(it) } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/adapter/TimeAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/adapter/TimeAdapter.kt new file mode 100644 index 00000000..69abca66 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/adapter/TimeAdapter.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-8 + */ + +package pl.szczodrzynski.edziennik.data.api.szkolny.adapter + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import pl.szczodrzynski.edziennik.utils.models.Time + +class TimeAdapter : TypeAdapter