Compare commits

...

25 Commits

Author SHA1 Message Date
810976d976 [3.9.4-dev] The Timetable Release 2019-11-11 19:22:15 +01:00
eb0540b5cb [Timetable] Fix adding NO_LESSONS in Mobidziennik. Change ID generation. 2019-11-11 19:14:02 +01:00
124437fd73 [Login] Allow fake login with DevMode. 2019-11-11 18:41:39 +01:00
69b512e3d1 [Timetable] Extract string resources. Increase offscreen page limit. 2019-11-11 18:32:49 +01:00
29d74e14bd [Timetable] Fix scrolling to first lesson. Update lesson ID generation. 2019-11-11 18:13:37 +01:00
f42ec8435a [Timetable] Show user friendly day name in view pager. 2019-11-11 15:46:04 +01:00
1052b824db [Timetable] Make it sync only timetable when getting a single week. 2019-11-11 14:52:09 +01:00
0742a6a74c [Timetable] Fix removing all lessons when not needed. 2019-11-11 14:16:31 +01:00
d4e9e1730f [APIv2] Implement getting timetable for one week. Support arguments in EdziennikTask. Create new DataRemoveModel. 2019-11-11 14:11:05 +01:00
4eeaa54a47 [Timetable] Implement Librus timetable with lesson changes and shifts. Update UI. 2019-11-10 22:57:19 +01:00
5fa7409317 [APIv2/Librus] Add getting normal lessons 2019-11-10 21:49:40 +01:00
0bcd190714 [APIv2] Add Librus Fake login. 2019-11-10 20:27:26 +01:00
563f08b0ab [UI] Update Timetable lesson layout. Add lesson number text. 2019-11-10 18:53:45 +01:00
1b75424604 [APIv2/UI] Add new Timetable module. Implement in Mobidziennik. 2019-11-10 17:53:10 +01:00
01ac26e67b [Sync] Fix background sync on Android O+. 2019-11-07 17:50:12 +01:00
434ddd1342 [3.9.2-dev] Add persistent debug logging. 2019-11-06 22:49:26 +01:00
3925496595 [3.9.1-dev] Fix Librus Messages crash. 2019-11-06 21:49:40 +01:00
5711c02170 [Home] Fix the private variable error. 2019-11-05 22:22:28 +01:00
ca1c691bf0 [3.9.0-dev] Update build.gradle and changelog 2019-11-05 22:12:15 +01:00
39c8a743bb [Login/Librus] Add Librus captcha activity/dialog. 2019-11-05 21:42:16 +01:00
14cd548dff [Sync] Add more AppManager intents to launch. 2019-11-05 18:49:40 +01:00
b72324805f [APIv2] Move onSuccess from callback to an argument 2019-11-05 18:20:41 +01:00
a049effa61 [APIv2/Librus] Add getting normal grade categories 2019-11-05 18:16:42 +01:00
23d55ec571 [APIv2/Vulcan] Add getting sent messages 2019-11-05 17:30:38 +01:00
385fe21d16 [APIv2/Librus] Implement error handling. Catch exceptions in ApiService. 2019-11-05 11:27:29 +01:00
77 changed files with 2680 additions and 337 deletions

View File

@ -159,11 +159,17 @@ dependencies {
//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 "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.linkedin.android.tachyon:tachyon:1.0.2'
}
repositories {
mavenCentral()

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF4caf50"
android:pathData="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/>
</vector>

View File

@ -15,10 +15,14 @@
android:theme="@style/SplashTheme"
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute">
<activity
android:name=".ui.modules.login.LoginLibrusCaptchaActivity"
android:theme="@android:style/Theme.Dialog"
android:excludeFromRecents="true"/>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/SplashTheme">
<intent-filter>
@ -29,7 +33,7 @@
</intent-filter>
</activity>
<activity
android:name="pl.szczodrzynski.edziennik.ui.modules.messages.MessagesComposeActivity"
android:name=".ui.modules.messages.MessagesComposeActivity"
android:configChanges="orientation|screenSize"
android:label="@string/messages_compose_title"
android:theme="@style/AppTheme.Black" />
@ -39,7 +43,7 @@
android:label="@string/app_name"
android:theme="@style/AppTheme" />
<activity
android:name="pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity"
android:name=".ui.modules.login.LoginActivity"
android:configChanges="orientation|screenSize"
android:launchMode="singleTop"
android:theme="@style/AppTheme.Light" />
@ -101,22 +105,18 @@
android:excludeFromRecents="true"
android:noHistory="true"
android:theme="@style/AppTheme.NoDisplay" />
<activity
android:name=".ui.modules.settings.SettingsLicenseActivity"
android:configChanges="orientation|keyboardHidden"
android:theme="@style/AppTheme" />
<activity
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
android:configChanges="orientation|keyboardHidden"
android:theme="@style/Base.Theme.AppCompat" />
<activity
android:name=".ui.modules.webpush.WebPushConfigActivity"
android:configChanges="orientation|keyboardHidden"
android:theme="@style/AppTheme.Dark" />
<activity
android:name=".ui.modules.home.CounterActivity"
android:theme="@style/AppTheme.Black" />
@ -169,7 +169,6 @@
android:name="android.appwidget.provider"
android:resource="@xml/widget_notifications_info" />
</receiver>
<receiver
android:name=".widgets.luckynumber.WidgetLuckyNumber"
android:label="@string/widget_lucky_number_title">
@ -188,7 +187,6 @@
<action android:name="android.intent.action.USER_PRESENT" />
</intent-filter>
</receiver>
<receiver android:name=".receivers.BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
@ -196,14 +194,7 @@
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<service
android:name=".sync.MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<receiver
android:name=".sync.FirebaseBroadcastReceiver"
android:exported="true"
@ -212,6 +203,23 @@
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</receiver>
<receiver
android:name=".receivers.SzkolnyReceiver"
android:exported="true">
<intent-filter>
<action android:name="pl.szczodrzynski.edziennik.SZKOLNY_MAIN" />
</intent-filter>
</receiver>
<service
android:name=".sync.MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<service
android:name=".widgets.timetable.WidgetTimetableService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
@ -222,14 +230,6 @@
<service android:name=".Notifier$GetDataRetryService" />
<receiver
android:name=".receivers.SzkolnyReceiver"
android:exported="true">
<intent-filter>
<action android:name="pl.szczodrzynski.edziennik.SZKOLNY_MAIN" />
</intent-filter>
</receiver>
<service android:name=".api.v2.ApiService" />
</application>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
@ -244,4 +244,4 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
</manifest>
</manifest>

View File

@ -31,57 +31,11 @@
</head>
<body>
<h3>Wersja 3.1.1, 2019-10-09</h3>
<h3>Wersja 4.0, 2019-jeszcze-nie-wiem-kiedy</h3>
<ul>
<li>Librus: poprawiona synchronizacja kategorii i kolorów ocen.</li>
<li>Zmieniony kolor dolnego paska w ciemnym motywie.</li>
<li>Zaktualizowany licznik czasu lekcji.</li>
</ul>
<h3>Wersja 3.1, 2019-09-29</h3>
<ul>
<li>Poprawiony interfejs zadań domowych.</li>
<li>Librus: wyświetlanie komentarzy ocen.</li>
<li>Librus: wyświetlanie nieobecności nauczycieli w Terminarzu.</li>
<li>Librus: usprawniona synchronizacja ocen.</li>
<li>Poprawki angielskiego tłumaczenia.</li>
</ul>
<h3>Wersja 3.0.3, 2019-09-26</h3>
<ul>
<li>Librus: poprawka kilku błędów synchronizacji.</li>
<li>Vulcan: prawidłowe oznaczanie wiadomości jako przeczytana.</li>
<li>Vulcan: poprawiona synchronizacja wiadomości i frekwencji.</li>
<li>Vulcan: poprawka błędów logowania.</li>
</ul>
<h3>Wersja 3.0.2, 2019-09-24</h3>
<ul>
<li>Librus: pobieranie Bieżących ocen opisowych.</li>
<li>Poprawki UI: kolor ikon paska statusu w jasnym motywie.</li>
<li>Poprawka braku skanera QR do przekazywania powiadomień.</li>
<li>Poprawka wyboru koloru i daty własnego wydarzenia, które crashowały aplikację.</li>
</ul>
<h3>Wersja 3.0.1, 2019-09-19</h3>
<ul>
<li>Librus: Poprawa błędu synchronizacji.</li>
<li>Poprawki UI związane z paskiem nawigacji.</li>
<li>Mobidziennik: Pobieranie ocen w niektórych przedmiotach.</li>
</ul>
<h3>Wersja 3.0, 2019-09-13</h3>
<ul>
<li><b>Nowy wygląd i sposób nawigacji</b> w całej aplikacji.</li>
<li>Menu nawigacji można teraz otworzyć przyciskiem na <b>dolnym pasku</b>. Pociągnięcie w górę tego paska wyświetla <b>menu kontekstowe</b> dotyczące danego widoku.</li>
<li>Założyliśmy serwer Discord! <a href="https://discord.gg/n9e8pWr">https://discord.gg/n9e8pWr</a></li>
<br>
<li>Librus: poprawka powielonych ogłoszeń szkolnych.</li>
<li>Naprawiłem błąd nieskończonej synchronizacji w Vulcanie.</li>
<li>Naprawiłem crash launchera przy dodaniu widgetu.</li>
<li>Naprawiłem częste crashe związane z widokiem kalendarza.</li>
<li>Nowe, ładniejsze (choć trochę) motywy kolorów.</li>
<li>Dużo drobnych poprawek UI i działania aplikacji.</li>
<li>UWAGA. To jest wersja in-development. Wiele funkcji może nie działać prawidłowo (lub wcale), co oznacza tylko że nie zostały jeszcze przeniesione
z wersji 3.x. Proszę o cierpliwość oraz <b>nie udostępnianie</b> tej wersji <u>nikomu</u>.</li>
<li>Bardzo dużo zmian</li>
</ul>
<!--<i>

View File

@ -36,6 +36,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;
@ -81,6 +82,7 @@ import pl.szczodrzynski.edziennik.network.NetworkUtils;
import pl.szczodrzynski.edziennik.network.TLSSocketFactory;
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;
@ -294,6 +296,10 @@ public class App extends androidx.multidex.MultiDexApplication implements Config
}
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);

View File

@ -6,8 +6,13 @@ import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.text.*
import android.text.style.ForegroundColorSpan
import android.text.style.StrikethroughSpan
import android.util.LongSparseArray
import android.util.SparseArray
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.app.ActivityCompat
import androidx.core.util.forEach
import com.google.gson.JsonArray
@ -326,4 +331,81 @@ fun String.crc32(): Long {
return crc.value
}
fun Long.formatDate(format: String = "yyyy-MM-dd HH:mm:ss"): String = SimpleDateFormat(format).format(this)
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
}
/**
* Returns a new read-only list only of those given elements, that are not empty.
* Applies for CharSequence and descendants.
*/
fun <T : CharSequence> listOfNotEmpty(vararg elements: T): List<T> = elements.filterNot { it.isEmpty() }
fun List<CharSequence>.concat(delimiter: String? = null): CharSequence {
if (this.isEmpty()) {
return ""
}
if (this.size == 1) {
return this[0]
}
var spanned = false
for (piece in this) {
if (piece is Spanned) {
spanned = true
break
}
}
var first = true
if (spanned) {
val ssb = SpannableStringBuilder()
for (piece in this) {
if (!first && delimiter != null)
ssb.append(delimiter)
first = false
ssb.append(piece)
}
return SpannedString(ssb)
} else {
val sb = StringBuilder()
for (piece in this) {
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<String, Any>): 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?.isNullOrEmpty(): Boolean = (this?.size() ?: 0) == 0
fun JsonArray.isEmpty(): Boolean = this.size() == 0

View File

@ -2,10 +2,15 @@ package pl.szczodrzynski.edziennik
import android.app.Activity
import android.app.ActivityManager
import android.content.*
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.provider.Settings
import android.view.Gravity
import android.view.View
import android.widget.Toast
@ -31,7 +36,6 @@ 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.api.v2.ApiService
import pl.szczodrzynski.edziennik.api.v2.events.*
import pl.szczodrzynski.edziennik.api.v2.events.task.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface.*
@ -58,12 +62,13 @@ import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
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.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.NavTarget
import pl.szczodrzynski.navlib.*
import pl.szczodrzynski.navlib.SystemBarsUtil.Companion.COLOR_HALF_TRANSPARENT
@ -518,17 +523,14 @@ class MainActivity : AppCompatActivity() {
fun syncCurrentFeature() {
swipeRefreshLayout.isRefreshing = true
Toast.makeText(this, fragmentToSyncName(navTargetId), Toast.LENGTH_SHORT).show()
ApiService.start(this)
val fragmentParam = when (navTargetId) {
DRAWER_ITEM_MESSAGES -> MessagesFragment.pageSelection
else -> 0
}
EventBus.getDefault().postSticky(
EdziennikTask.syncProfile(
App.profileId,
listOf(navTargetId to fragmentParam)
)
)
EdziennikTask.syncProfile(
App.profileId,
listOf(navTargetId to fragmentParam)
).enqueue(this)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onSyncStartedEvent(event: ApiTaskStartedEvent) {
@ -582,29 +584,18 @@ class MainActivity : AppCompatActivity() {
.setMessage(R.string.app_manager_dialog_text)
.setPositiveButton(R.string.ok) { dialog, which ->
try {
val intent = Intent()
intent.component = ComponentName(
"com.huawei.systemmanager",
"com.huawei.systemmanager.appcontrol.activity.StartupAppControlActivity"
)
startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
try {
val intent = Intent()
intent.component = ComponentName(
"com.asus.mobilemanager",
"com.asus.mobilemanager.MainActivity"
)
startActivity(intent)
} catch (e: Exception) {
try {
startActivity(Intent(android.provider.Settings.ACTION_SETTINGS))
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(this, R.string.app_manager_open_failed, Toast.LENGTH_SHORT).show()
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 ->
@ -788,7 +779,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) {
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)

View File

@ -90,6 +90,7 @@ class ApiService : Service() {
errorList.add(apiError)
apiError.throwable?.printStackTrace()
if (apiError.isCritical) {
taskRunning?.cancel()
notification.setCriticalError().post()
taskRunning = null
taskIsRunning = false
@ -154,10 +155,14 @@ class ApiService : Service() {
// post an event
EventBus.getDefault().post(ApiTaskStartedEvent(taskProfileId, task.profile))
when (task) {
is EdziennikTask -> task.run(app, taskCallback)
is NotifyTask -> task.run(app, taskCallback)
is ErrorReportTask -> task.run(app, taskCallback, notification, errorList)
try {
when (task) {
is EdziennikTask -> task.run(app, taskCallback)
is NotifyTask -> task.run(app, taskCallback)
is ErrorReportTask -> task.run(app, taskCallback, notification, errorList)
}
} catch (e: Exception) {
taskCallback.onError(ApiError(TAG, EXCEPTION_API_TASK).withThrowable(e))
}
}
@ -227,11 +232,13 @@ class ApiService : Service() {
____) | __/ | \ 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
}

View File

@ -14,6 +14,14 @@ val SYSTEM_USER_AGENT = System.getProperty("http.agent") ?: "Dalvik/2.1.0 Androi
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"

View File

@ -150,6 +150,7 @@ const val ERROR_IDZIENNIK_API_OTHER = 451
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

View File

@ -66,13 +66,13 @@ val librusLoginMethods = listOf(
},
LoginMethod(LOGIN_TYPE_LIBRUS, LOGIN_METHOD_LIBRUS_SYNERGIA, LibrusLoginSynergia::class.java)
.withIsPossible { _, _ -> true }
.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 { _, _ -> true }
.withIsPossible { _, loginStore -> !loginStore.hasLoginData("fakeLogin") }
.withRequiredLoginMethod { profile, _ ->
if (profile?.hasStudentData("accountPassword") == false) LOGIN_METHOD_LIBRUS_SYNERGIA else LOGIN_METHOD_NOT_NEEDED
}

View File

@ -1,5 +1,6 @@
package pl.szczodrzynski.edziennik.api.v2.events.task
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.api.v2.*
@ -18,7 +19,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
fun firstLogin(loginStore: LoginStore) = EdziennikTask(-1, FirstLoginRequest(loginStore))
fun sync() = EdziennikTask(-1, SyncRequest())
fun syncProfile(profileId: Int, viewIds: List<Pair<Int, Int>>? = null) = EdziennikTask(profileId, SyncProfileRequest(viewIds))
fun syncProfile(profileId: Int, viewIds: List<Pair<Int, Int>>? = null, arguments: JsonObject? = null) = EdziennikTask(profileId, SyncProfileRequest(viewIds, arguments))
fun syncProfileList(profileList: List<Int>) = EdziennikTask(-1, SyncProfileListRequest(profileList))
fun messageGet(profileId: Int, messageId: Int) = EdziennikTask(profileId, MessageGetRequest(messageId))
fun announcementsRead(profileId: Int) = EdziennikTask(profileId, AnnouncementsReadRequest())
@ -50,7 +51,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
private var edziennikInterface: EdziennikInterface? = null
fun run(app: App, taskCallback: EdziennikCallback) {
internal fun run(app: App, taskCallback: EdziennikCallback) {
edziennikInterface = when (loginStore.type) {
LOGIN_TYPE_LIBRUS -> Librus(app, profile, loginStore, taskCallback)
LOGIN_TYPE_MOBIDZIENNIK -> Mobidziennik(app, profile, loginStore, taskCallback)
@ -66,7 +67,8 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
when (request) {
is SyncProfileRequest -> edziennikInterface?.sync(
featureIds = request.viewIds?.flatMap { Features.getIdsByView(it.first, it.second) } ?: Features.getAllIds(),
viewId = request.viewIds?.get(0)?.first)
viewId = request.viewIds?.get(0)?.first,
arguments = request.arguments)
is MessageGetRequest -> edziennikInterface?.getMessage(request.messageId)
is FirstLoginRequest -> edziennikInterface?.firstLogin()
is AnnouncementsReadRequest -> edziennikInterface?.markAllAnnouncementsAsRead()
@ -83,7 +85,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
data class FirstLoginRequest(val loginStore: LoginStore)
class SyncRequest
data class SyncProfileRequest(val viewIds: List<Pair<Int, Int>>? = null)
data class SyncProfileRequest(val viewIds: List<Pair<Int, Int>>? = null, val arguments: JsonObject? = null)
data class SyncProfileListRequest(val profileList: List<Int>)
data class MessageGetRequest(val messageId: Int)
class AnnouncementsReadRequest

View File

@ -6,6 +6,8 @@ package pl.szczodrzynski.edziennik.api.v2.events.task
import android.content.Context
import android.content.Intent
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.api.v2.ApiService
@ -25,7 +27,12 @@ abstract class IApiTask(open val profileId: Int) {
abstract fun cancel()
fun enqueue(context: Context) {
context.startService(Intent(context, ApiService::class.java))
Intent(context, ApiService::class.java).let {
if (SDK_INT >= O)
context.startForegroundService(it)
else
context.startService(it)
}
EventBus.getDefault().postSticky(this)
}

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.api.v2.idziennik
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.api.v2.CODE_INTERNAL_LIBRUS_ACCOUNT_410
import pl.szczodrzynski.edziennik.api.v2.idziennik.data.IdziennikData
@ -48,7 +49,8 @@ class Idziennik(val app: App, val profile: Profile?, val loginStore: LoginStore,
|_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_|
__/ |
|__*/
override fun sync(featureIds: List<Int>, viewId: Int?) {
override fun sync(featureIds: List<Int>, viewId: Int?, arguments: JsonObject?) {
data.arguments = arguments
data.prepare(idziennikLoginMethods, IdziennikFeatures, featureIds, viewId)
d(TAG, "LoginMethod IDs: ${data.targetLoginMethodIds}")
d(TAG, "Endpoint IDs: ${data.targetEndpointIds}")

View File

@ -41,43 +41,43 @@ class IdziennikData(val data: DataIdziennik, val onSuccess: () -> Unit) {
when (endpointId) {
ENDPOINT_IDZIENNIK_WEB_TIMETABLE -> {
data.startProgress(R.string.edziennik_progress_endpoint_timetable)
IdziennikWebTimetable(data) { onSuccess() }
IdziennikWebTimetable(data, onSuccess)
}
ENDPOINT_IDZIENNIK_WEB_GRADES -> {
data.startProgress(R.string.edziennik_progress_endpoint_grades)
IdziennikWebGrades(data) { onSuccess() }
IdziennikWebGrades(data, onSuccess)
}
ENDPOINT_IDZIENNIK_WEB_PROPOSED_GRADES -> {
data.startProgress(R.string.edziennik_progress_endpoint_proposed_grades)
IdziennikWebProposedGrades(data) { onSuccess() }
IdziennikWebProposedGrades(data, onSuccess)
}
ENDPOINT_IDZIENNIK_WEB_EXAMS -> {
data.startProgress(R.string.edziennik_progress_endpoint_exams)
IdziennikWebExams(data) { onSuccess() }
IdziennikWebExams(data, onSuccess)
}
ENDPOINT_IDZIENNIK_WEB_NOTICES -> {
data.startProgress(R.string.edziennik_progress_endpoint_notices)
IdziennikWebNotices(data) { onSuccess() }
IdziennikWebNotices(data, onSuccess)
}
ENDPOINT_IDZIENNIK_WEB_ANNOUNCEMENTS -> {
data.startProgress(R.string.edziennik_progress_endpoint_announcements)
IdziennikWebAnnouncements(data) { onSuccess() }
IdziennikWebAnnouncements(data, onSuccess)
}
ENDPOINT_IDZIENNIK_WEB_ATTENDANCE -> {
data.startProgress(R.string.edziennik_progress_endpoint_attendance)
IdziennikWebAttendance(data) { onSuccess() }
IdziennikWebAttendance(data, onSuccess)
}
ENDPOINT_IDZIENNIK_API_CURRENT_REGISTER -> {
data.startProgress(R.string.edziennik_progress_endpoint_lucky_number)
IdziennikApiCurrentRegister(data) { onSuccess() }
IdziennikApiCurrentRegister(data, onSuccess)
}
ENDPOINT_IDZIENNIK_API_MESSAGES_INBOX -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox)
IdziennikApiMessagesInbox(data) { onSuccess() }
IdziennikApiMessagesInbox(data, onSuccess)
}
ENDPOINT_IDZIENNIK_API_MESSAGES_SENT -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_outbox)
IdziennikApiMessagesSent(data) { onSuccess() }
IdziennikApiMessagesSent(data, onSuccess)
}
else -> onSuccess()
}

View File

@ -4,8 +4,10 @@
package pl.szczodrzynski.edziennik.api.v2.interfaces
import com.google.gson.JsonObject
interface EdziennikInterface {
fun sync(featureIds: List<Int>, viewId: Int? = null)
fun sync(featureIds: List<Int>, viewId: Int? = null, arguments: JsonObject? = null)
fun getMessage(messageId: Int)
fun markAllAnnouncementsAsRead()
fun firstLogin()

View File

@ -4,8 +4,9 @@
package pl.szczodrzynski.edziennik.api.v2.librus
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.api.v2.CODE_INTERNAL_LIBRUS_ACCOUNT_410
import pl.szczodrzynski.edziennik.api.v2.*
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.api.v2.librus.data.LibrusData
@ -14,9 +15,7 @@ import pl.szczodrzynski.edziennik.api.v2.librus.firstlogin.LibrusFirstLogin
import pl.szczodrzynski.edziennik.api.v2.librus.login.LibrusLogin
import pl.szczodrzynski.edziennik.api.v2.librus.login.LibrusLoginApi
import pl.szczodrzynski.edziennik.api.v2.librus.login.LibrusLoginSynergia
import pl.szczodrzynski.edziennik.api.v2.librusLoginMethods
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.api.v2.prepare
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.utils.Utils.d
@ -51,14 +50,31 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
|_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_|
__/ |
|__*/
override fun sync(featureIds: List<Int>, viewId: Int?) {
override fun sync(featureIds: List<Int>, viewId: Int?, arguments: JsonObject?) {
data.arguments = arguments
data.prepare(librusLoginMethods, LibrusFeatures, featureIds, viewId)
d(TAG, "LoginMethod IDs: ${data.targetLoginMethodIds}")
d(TAG, "Endpoint IDs: ${data.targetEndpointIds}")
login()
}
private fun login() {
d(TAG, "Trying to login with ${data.targetLoginMethodIds}")
if (internalErrorList.isNotEmpty()) {
d(TAG, " - Internal errors:")
internalErrorList.forEach { d(TAG, " - code $it") }
}
LibrusLogin(data) {
LibrusData(data) {
completed()
}
data()
}
}
private fun data() {
d(TAG, "Endpoint IDs: ${data.targetEndpointIds}")
if (internalErrorList.isNotEmpty()) {
d(TAG, " - Internal errors:")
internalErrorList.forEach { d(TAG, " - code $it") }
}
LibrusData(data) {
completed()
}
}
@ -102,15 +118,70 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
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) {
in internalErrorList -> {
// finish immediately if the same error occurs twice during the same sync
callback.onError(apiError)
ERROR_LIBRUS_PORTAL_ACCESS_DENIED -> {
data.loginMethods.remove(LOGIN_METHOD_LIBRUS_PORTAL)
data.targetLoginMethodIds.add(LOGIN_METHOD_LIBRUS_PORTAL)
data.targetLoginMethodIds.sort()
data.portalTokenExpiryTime = 0
login()
}
CODE_INTERNAL_LIBRUS_ACCOUNT_410 -> {
internalErrorList.add(apiError.errorCode)
loginStore.removeLoginData("refreshToken") // force a clean login
//loginLibrus()
ERROR_LIBRUS_API_ACCESS_DENIED,
ERROR_LIBRUS_API_TOKEN_EXPIRED -> {
data.loginMethods.remove(LOGIN_METHOD_LIBRUS_API)
data.targetLoginMethodIds.add(LOGIN_METHOD_LIBRUS_API)
data.targetLoginMethodIds.sort()
data.apiTokenExpiryTime = 0
login()
}
ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED -> {
data.loginMethods.remove(LOGIN_METHOD_LIBRUS_SYNERGIA)
data.targetLoginMethodIds.add(LOGIN_METHOD_LIBRUS_SYNERGIA)
data.targetLoginMethodIds.sort()
data.synergiaSessionIdExpiryTime = 0
login()
}
ERROR_LIBRUS_MESSAGES_ACCESS_DENIED -> {
data.loginMethods.remove(LOGIN_METHOD_LIBRUS_MESSAGES)
data.targetLoginMethodIds.add(LOGIN_METHOD_LIBRUS_MESSAGES)
data.targetLoginMethodIds.sort()
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 -> {
loginStore.putLoginData("timetableNotPublic", true)
data()
}
ERROR_LIBRUS_API_LUCKY_NUMBER_NOT_ACTIVE,
ERROR_LIBRUS_API_NOTES_NOT_ACTIVE -> {
data()
}
else -> callback.onError(apiError)
}

View File

@ -28,7 +28,7 @@ open class LibrusApi(open val data: DataLibrus) {
fun apiGet(tag: String, endpoint: String, method: Int = GET, payload: JsonObject? = null, onSuccess: (json: JsonObject) -> Unit) {
d(tag, "Request: Librus/Api - $LIBRUS_API_URL/$endpoint")
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?) {
@ -90,7 +90,7 @@ open class LibrusApi(open val data: DataLibrus) {
}
Request.builder()
.url("$LIBRUS_API_URL/$endpoint")
.url("${if (data.fakeLogin) FAKE_LIBRUS_API else LIBRUS_API_URL}/$endpoint")
.userAgent(LIBRUS_USER_AGENT)
.addHeader("Authorization", "Bearer ${data.apiAccessToken}")
.apply {

View File

@ -45,92 +45,99 @@ class LibrusData(val data: DataLibrus, val onSuccess: () -> Unit) {
*/
ENDPOINT_LIBRUS_API_ME -> {
data.startProgress(R.string.edziennik_progress_endpoint_student_info)
LibrusApiMe(data) { onSuccess() }
LibrusApiMe(data, onSuccess)
}
ENDPOINT_LIBRUS_API_SCHOOLS -> {
data.startProgress(R.string.edziennik_progress_endpoint_school_info)
LibrusApiSchools(data) { onSuccess() }
LibrusApiSchools(data, onSuccess)
}
ENDPOINT_LIBRUS_API_CLASSES -> {
data.startProgress(R.string.edziennik_progress_endpoint_classes)
LibrusApiClasses(data) { onSuccess() }
LibrusApiClasses(data, onSuccess)
}
ENDPOINT_LIBRUS_API_VIRTUAL_CLASSES -> {
data.startProgress(R.string.edziennik_progress_endpoint_teams)
LibrusApiVirtualClasses(data) { onSuccess() }
LibrusApiVirtualClasses(data, onSuccess)
}
ENDPOINT_LIBRUS_API_UNITS -> {
data.startProgress(R.string.edziennik_progress_endpoint_units)
LibrusApiUnits(data) { onSuccess() }
LibrusApiUnits(data, onSuccess)
}
ENDPOINT_LIBRUS_API_USERS -> {
data.startProgress(R.string.edziennik_progress_endpoint_teachers)
LibrusApiUsers(data) { onSuccess() }
LibrusApiUsers(data, onSuccess)
}
ENDPOINT_LIBRUS_API_SUBJECTS -> {
data.startProgress(R.string.edziennik_progress_endpoint_subjects)
LibrusApiSubjects(data) { onSuccess() }
LibrusApiSubjects(data, onSuccess)
}
ENDPOINT_LIBRUS_API_CLASSROOMS -> {
data.startProgress(R.string.edziennik_progress_endpoint_classrooms)
LibrusApiClassrooms(data) { onSuccess() }
LibrusApiClassrooms(data, onSuccess)
}
// TODO push config
// TODO timetable
ENDPOINT_LIBRUS_API_TIMETABLES -> {
data.startProgress(R.string.edziennik_progress_endpoint_timetable)
LibrusApiTimetables(data, onSuccess)
}
ENDPOINT_LIBRUS_API_NORMAL_GRADES -> {
data.startProgress(R.string.edziennik_progress_endpoint_grades)
LibrusApiGrades(data) { onSuccess() }
LibrusApiGrades(data, onSuccess)
}
ENDPOINT_LIBRUS_API_NORMAL_GC -> {
data.startProgress(R.string.edziennik_progress_endpoint_grade_categories)
LibrusApiGradeCategories(data, onSuccess)
}
// TODO grades
ENDPOINT_LIBRUS_API_EVENT_TYPES -> {
data.startProgress(R.string.edziennik_progress_endpoint_event_types)
LibrusApiEventTypes(data) { onSuccess() }
LibrusApiEventTypes(data, onSuccess)
}
ENDPOINT_LIBRUS_API_EVENTS -> {
data.startProgress(R.string.edziennik_progress_endpoint_events)
LibrusApiEvents(data) { onSuccess() }
LibrusApiEvents(data, onSuccess)
}
ENDPOINT_LIBRUS_API_HOMEWORK -> {
data.startProgress(R.string.edziennik_progress_endpoint_homework)
LibrusApiHomework(data) { onSuccess() }
LibrusApiHomework(data, onSuccess)
}
ENDPOINT_LIBRUS_API_LUCKY_NUMBER -> {
data.startProgress(R.string.edziennik_progress_endpoint_lucky_number)
LibrusApiLuckyNumber(data) { onSuccess() }
LibrusApiLuckyNumber(data, onSuccess)
}
ENDPOINT_LIBRUS_API_NOTICE_TYPES -> {
data.startProgress(R.string.edziennik_progress_endpoint_notice_types)
LibrusApiNoticeTypes(data) { onSuccess() }
LibrusApiNoticeTypes(data, onSuccess)
}
ENDPOINT_LIBRUS_API_NOTICES -> {
data.startProgress(R.string.edziennik_progress_endpoint_notices)
LibrusApiNotices(data) { onSuccess() }
LibrusApiNotices(data, onSuccess)
}
ENDPOINT_LIBRUS_API_ATTENDANCE_TYPES -> {
data.startProgress(R.string.edziennik_progress_endpoint_attendance_types)
LibrusApiAttendanceTypes(data) { onSuccess() }
LibrusApiAttendanceTypes(data, onSuccess)
}
ENDPOINT_LIBRUS_API_ATTENDANCES -> {
data.startProgress(R.string.edziennik_progress_endpoint_attendance)
LibrusApiAttendances(data) { onSuccess() }
LibrusApiAttendances(data, onSuccess)
}
ENDPOINT_LIBRUS_API_ANNOUNCEMENTS -> {
data.startProgress(R.string.edziennik_progress_endpoint_announcements)
LibrusApiAnnouncements(data) { onSuccess() }
LibrusApiAnnouncements(data, onSuccess)
}
ENDPOINT_LIBRUS_API_PT_MEETINGS -> {
data.startProgress(R.string.edziennik_progress_endpoint_pt_meetings)
LibrusApiPtMeetings(data) { onSuccess() }
LibrusApiPtMeetings(data, onSuccess)
}
ENDPOINT_LIBRUS_API_TEACHER_FREE_DAY_TYPES -> {
data.startProgress(R.string.edziennik_progress_endpoint_teacher_free_day_types)
LibrusApiTeacherFreeDayTypes(data) { onSuccess() }
LibrusApiTeacherFreeDayTypes(data, onSuccess)
}
ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS -> {
data.startProgress(R.string.edziennik_progress_endpoint_teacher_free_days)
LibrusApiTeacherFreeDays(data) { onSuccess() }
LibrusApiTeacherFreeDays(data, onSuccess)
}
/**
@ -138,11 +145,11 @@ class LibrusData(val data: DataLibrus, val onSuccess: () -> Unit) {
*/
ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK -> {
data.startProgress(R.string.edziennik_progress_endpoint_homework)
LibrusSynergiaHomework(data) { onSuccess() }
LibrusSynergiaHomework(data, onSuccess)
}
ENDPOINT_LIBRUS_SYNERGIA_INFO -> {
data.startProgress(R.string.edziennik_progress_endpoint_student_info)
LibrusSynergiaInfo(data) { onSuccess() }
LibrusSynergiaInfo(data, onSuccess)
}
/**
@ -150,11 +157,11 @@ class LibrusData(val data: DataLibrus, val onSuccess: () -> Unit) {
*/
ENDPOINT_LIBRUS_MESSAGES_RECEIVED -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox)
LibrusMessagesGetList(data, type = Message.TYPE_RECEIVED) { onSuccess() }
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() }
LibrusMessagesGetList(data, type = Message.TYPE_SENT, onSuccess = onSuccess)
}
else -> onSuccess()

View File

@ -12,13 +12,17 @@ import okhttp3.Cookie
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.parser.Parser
import org.redundent.kotlin.xml.PrintOptions
import org.redundent.kotlin.xml.xml
import pl.szczodrzynski.edziennik.api.v2.*
import pl.szczodrzynski.edziennik.api.v2.librus.DataLibrus
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.get
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
open class LibrusMessages(open val data: DataLibrus) {
companion object {
@ -80,7 +84,27 @@ open class LibrusMessages(open val data: DataLibrus) {
.secure().httpOnly().build()
))
val requestXml = xml("service") {
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()
/*val requestXml = xml("service") {
"header" { }
"data" {
for ((key, value) in parameters.orEmpty()) {
@ -92,7 +116,7 @@ open class LibrusMessages(open val data: DataLibrus) {
}.toString(PrintOptions(
singleLineTextElements = true,
useSelfClosingTags = true
))
))*/
Request.builder()
.url("$LIBRUS_MESSAGES_URL/$endpoint")

View File

@ -24,7 +24,7 @@ open class LibrusPortal(open val data: DataLibrus) {
fun portalGet(tag: String, endpoint: String, method: Int = GET, payload: JsonObject? = null, onSuccess: (json: JsonObject, response: Response?) -> Unit) {
d(tag, "Request: Librus/Portal - $LIBRUS_PORTAL_URL$endpoint")
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?) {
@ -44,7 +44,10 @@ open class LibrusPortal(open val data: DataLibrus) {
"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 -> ERROR_LIBRUS_PORTAL_OTHER
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)
@ -78,7 +81,7 @@ open class LibrusPortal(open val data: DataLibrus) {
}
Request.builder()
.url(LIBRUS_PORTAL_URL + endpoint)
.url((if (data.fakeLogin) FAKE_LIBRUS_PORTAL else LIBRUS_PORTAL_URL) + endpoint)
.userAgent(LIBRUS_USER_AGENT)
.addHeader("Authorization", "Bearer ${data.portalAccessToken}")
.apply {

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-11-5
*/
package pl.szczodrzynski.edziennik.api.v2.librus.data.api
import android.graphics.Color
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.api.v2.librus.DataLibrus
import pl.szczodrzynski.edziennik.api.v2.librus.ENDPOINT_LIBRUS_API_NORMAL_GC
import pl.szczodrzynski.edziennik.api.v2.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") ?: ""
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_GC, SYNC_ALWAYS)
onSuccess()
}
}
}

View File

@ -0,0 +1,192 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-10.
*/
package pl.szczodrzynski.edziennik.api.v2.librus.data.api
import androidx.core.util.isEmpty
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.api.v2.librus.DataLibrus
import pl.szczodrzynski.edziennik.api.v2.librus.ENDPOINT_LIBRUS_API_TIMETABLES
import pl.szczodrzynski.edziennik.api.v2.librus.data.LibrusApi
import pl.szczodrzynski.edziennik.api.v2.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
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 = Date.getToday().let { it.stepForward(0, 0, -it.weekDay) }
val getDate = data.arguments?.getString("weekStart") ?: currentWeekStart.stringY_m_d
apiGet(TAG, "Timetables?weekStart=$getDate") { 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?.forEachIndexed { index, lesson ->
parseLesson(lessonDate, lesson)
}
}
if (day.isNullOrEmpty() || !lessonsFound) {
data.lessonNewList += Lesson(profileId, lessonDate.value.toLong()).apply {
type = Lesson.TYPE_NO_LESSONS
date = lessonDate
}
}
}
val weekStart = Date.fromY_m_d(getDate)
val weekEnd = weekStart.clone().stepForward(0, 0, 6)
d(TAG, "Clearing lessons between ${weekStart.stringY_m_d} and ${weekEnd.stringY_m_d} - timetable downloaded for $getDate")
data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd))
data.setSyncNext(ENDPOINT_LIBRUS_API_TIMETABLES, SYNC_ALWAYS)
onSuccess()
}
}
private fun parseLesson(lessonDate: Date, lesson: JsonObject) {
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 id = lessonDate.combineWith(startTime) / 6L * 10L + (lesson.hashCode() and 0xFFFF)
val lessonObject = Lesson(profileId, id)
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
}
}
if (lessonObject.type != Lesson.TYPE_NORMAL) {
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_LESSON_CHANGE,
lessonObject.id,
data.profile?.empty ?: false,
data.profile?.empty ?: false,
System.currentTimeMillis()
))
}
data.lessonNewList += lessonObject
}
}

View File

@ -3,6 +3,7 @@ package pl.szczodrzynski.edziennik.api.v2.librus.firstlogin
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.api.v2.ERROR_NO_STUDENTS_IN_ACCOUNT
import pl.szczodrzynski.edziennik.api.v2.FAKE_LIBRUS_ACCOUNTS
import pl.szczodrzynski.edziennik.api.v2.LIBRUS_ACCOUNTS_URL
import pl.szczodrzynski.edziennik.api.v2.LOGIN_MODE_LIBRUS_EMAIL
import pl.szczodrzynski.edziennik.api.v2.events.FirstLoginFinishedEvent
@ -29,7 +30,7 @@ class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) {
if (data.loginStore.mode == LOGIN_MODE_LIBRUS_EMAIL) {
// email login: use Portal for account list
LibrusLoginPortal(data) {
portal.portalGet(TAG, LIBRUS_ACCOUNTS_URL) { json, response ->
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) {

View File

@ -7,14 +7,15 @@ 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.*
import pl.szczodrzynski.edziennik.api.v2.*
import pl.szczodrzynski.edziennik.api.v2.librus.DataLibrus
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.utils.Utils
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.ArrayList
import java.util.*
import java.util.regex.Pattern
class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
@ -42,7 +43,7 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
}
else {
data.app.cookieJar.clearForDomain("portal.librus.pl")
authorize(LIBRUS_AUTHORIZE_URL)
authorize(if (data.fakeLogin) FAKE_LIBRUS_AUTHORIZE else LIBRUS_AUTHORIZE_URL)
}
}}
@ -86,10 +87,10 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
}
private fun login(csrfToken: String) {
d(TAG, "Request: Librus/Login/Portal - $LIBRUS_LOGIN_URL")
d(TAG, "Request: Librus/Login/Portal - ${if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL}")
Request.builder()
.url(LIBRUS_LOGIN_URL)
.url(if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL)
.userAgent(LIBRUS_USER_AGENT)
.addParameter("email", data.portalEmail)
.addParameter("password", data.portalPassword)
@ -135,7 +136,7 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
private var refreshTokenFailed = false
private fun accessToken(code: String?, refreshToken: String?) {
d(TAG, "Request: Librus/Login/Portal - $LIBRUS_TOKEN_URL")
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")
@ -204,7 +205,7 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
}
Request.builder()
.url(LIBRUS_TOKEN_URL)
.url(if (data.fakeLogin) FAKE_LIBRUS_TOKEN else LIBRUS_TOKEN_URL)
.userAgent(LIBRUS_USER_AGENT)
.addParams(params)
.post()

View File

@ -43,7 +43,7 @@ class SynergiaTokenExtractor(override val data: DataLibrus, val onSuccess: () ->
val accountLogin = data.apiLogin ?: return false
data.portalAccessToken ?: return false
d(TAG, "Request: Librus/SynergiaTokenExtractor - $LIBRUS_ACCOUNT_URL$accountLogin")
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
@ -67,7 +67,7 @@ class SynergiaTokenExtractor(override val data: DataLibrus, val onSuccess: () ->
}
}
portalGet(TAG, LIBRUS_ACCOUNT_URL+accountLogin, onSuccess = onSuccess)
portalGet(TAG, (if (data.fakeLogin) FAKE_LIBRUS_ACCOUNT else LIBRUS_ACCOUNT_URL)+accountLogin, onSuccess = onSuccess)
return true
}
}

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.api.v2.mobidziennik
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.api.v2.CODE_INTERNAL_LIBRUS_ACCOUNT_410
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikCallback
@ -48,7 +49,8 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto
|_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_|
__/ |
|__*/
override fun sync(featureIds: List<Int>, viewId: Int?) {
override fun sync(featureIds: List<Int>, viewId: Int?, arguments: JsonObject?) {
data.arguments = arguments
data.prepare(mobidziennikLoginMethods, MobidziennikFeatures, featureIds, viewId)
d(TAG, "LoginMethod IDs: ${data.targetLoginMethodIds}")
d(TAG, "Endpoint IDs: ${data.targetEndpointIds}")

View File

@ -21,10 +21,10 @@ const val ENDPOINT_MOBIDZIENNIK_API2_MAIN = 3000
val MobidziennikFeatures = listOf(
// always synced
/*Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_ALWAYS_NEEDED, listOf(
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)),*/
), 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(

View File

@ -42,7 +42,7 @@ class MobidziennikData(val data: DataMobidziennik, val onSuccess: () -> Unit) {
when (endpointId) {
ENDPOINT_MOBIDZIENNIK_API_MAIN -> {
data.startProgress(R.string.edziennik_progress_endpoint_data)
MobidziennikApi(data) { onSuccess() }
MobidziennikApi(data, onSuccess)
}
ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_INBOX -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox)
@ -75,4 +75,4 @@ class MobidziennikData(val data: DataMobidziennik, val onSuccess: () -> Unit) {
else -> onSuccess()
}
}
}
}

View File

@ -4,16 +4,102 @@
package pl.szczodrzynski.edziennik.api.v2.mobidziennik.data.api
import pl.szczodrzynski.edziennik.App.profileId
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.data.db.modules.lessons.Lesson
import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange
import pl.szczodrzynski.edziennik.api.v2.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<String>) {
init {
for (lessonStr in rows) {
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<Int>()
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])
val id = date.combineWith(startTime) / 6L * 10L + (lesson.joinToString("|").hashCode() and 0xFFFF)
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, id).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
}
}
if (it.type != Lesson.TYPE_NORMAL) {
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_LESSON_CHANGE,
it.id,
data.profile?.empty ?: false,
data.profile?.empty ?: false,
System.currentTimeMillis()
))
}
data.lessonNewList += it
}
}
for (day in dataDays) {
val lessonDate = Date.fromValue(day)
data.lessonNewList += Lesson(profileId, lessonDate.value.toLong()).apply {
type = Lesson.TYPE_NO_LESSONS
date = lessonDate
}
}
/*for (lessonStr in rows) {
if (lessonStr.isNotEmpty()) {
val lesson = lessonStr.split("|")
@ -76,9 +162,9 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
if (originalLesson == null) {
// original lesson doesn't exist, save a new addition
// TODO
/*if (!RegisterLessonChange.existsAddition(app.profile, registerLessonChange)) {
*//*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)) {
@ -108,6 +194,6 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
}
}
}
}
}*/
}
}

View File

@ -60,6 +60,8 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
val profileId
get() = profile?.id ?: -1
var arguments: JsonObject? = null
/**
* A callback passed to all [Feature]s and [LoginMethod]s
*/
@ -133,23 +135,20 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
mTeamClass = value
}
var lessonsToRemove: DataRemoveModel? = null
var toRemove = mutableListOf<DataRemoveModel>()
val lessonList = mutableListOf<Lesson>()
val lessonChangeList = mutableListOf<LessonChange>()
val lessonNewList = mutableListOf<pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson>()
var gradesToRemove: DataRemoveModel? = null
val gradeList = mutableListOf<Grade>()
var eventsToRemove: DataRemoveModel? = null
val eventList = mutableListOf<Event>()
var noticesToRemove: DataRemoveModel? = null
val noticeList = mutableListOf<Notice>()
var attendancesToRemove: DataRemoveModel? = null
val attendanceList = mutableListOf<Attendance>()
var announcementsToRemove: DataRemoveModel? = null
val announcementList = mutableListOf<Announcement>()
val luckyNumberList = mutableListOf<LuckyNumber>()
@ -166,6 +165,9 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
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()
@ -180,6 +182,7 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
fun clear() {
loginMethods.clear()
toRemove.clear()
endpointTimers.clear()
teacherList.clear()
subjectList.clear()
@ -195,6 +198,7 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
lessonList.clear()
lessonChangeList.clear()
lessonNewList.clear()
gradeList.clear()
noticeList.clear()
attendanceList.clear()
@ -248,6 +252,7 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
app.profile.loginStoreData = loginStore.data
}
// always present and not empty, during every sync
db.endpointTimerDao().addAll(endpointTimers)
db.teacherDao().clear(profileId)
db.teacherDao().addAll(teacherList.values())
@ -260,6 +265,7 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
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)
@ -271,17 +277,24 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
if (teacherAbsenceTypes.size > 0)
db.teacherAbsenceTypeDao().addAll(teacherAbsenceTypes.values())
gradesToRemove?.let { it ->
it.removeAll?.let { _ -> db.gradeDao().clear(profileId) }
it.removeSemester?.let { semester -> db.gradeDao().clearForSemester(profileId, semester) }
// 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())
}
}
// 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)
}
@ -390,8 +403,6 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
}
fun error(apiError: ApiError) {
if (apiError.isCritical)
cancel()
callback.onError(apiError)
}

View File

@ -4,28 +4,37 @@
package pl.szczodrzynski.edziennik.api.v2.models
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
class DataRemoveModel {
var removeAll: Boolean? = null
var removeSemester: Int? = null
var removeDateFrom: Date? = null
var removeDateTo: Date? = null
constructor() {
this.removeAll = true
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) }
}
}
}
constructor(semester: Int) {
this.removeSemester = semester
}
constructor(dateFrom: Date?, dateTo: Date) {
this.removeDateFrom = dateFrom
this.removeDateTo = dateTo
}
constructor(dateFrom: Date) {
this.removeDateFrom = dateFrom
class Grades(val all: Boolean, val semester: Int?) : DataRemoveModel() {
companion object {
fun all() = Grades(true, null)
fun semester(semester: Int) = Grades(false, semester)
}
fun commit(profileId: Int, dao: GradeDao) {
if (all) {
dao.clear(profileId)
}
semester?.let { dao.clearForSemester(profileId, it) }
}
}
}

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.api.v2.template
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.api.v2.CODE_INTERNAL_LIBRUS_ACCOUNT_410
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikCallback
@ -48,7 +49,8 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore,
|_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_|
__/ |
|__*/
override fun sync(featureIds: List<Int>, viewId: Int?) {
override fun sync(featureIds: List<Int>, 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}")

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.api.v2.vulcan
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.api.v2.CODE_INTERNAL_LIBRUS_ACCOUNT_410
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikCallback
@ -48,7 +49,8 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
|_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_|
__/ |
|__*/
override fun sync(featureIds: List<Int>, viewId: Int?) {
override fun sync(featureIds: List<Int>, viewId: Int?, arguments: JsonObject?) {
data.arguments = arguments
data.prepare(vulcanLoginMethods, VulcanFeatures, featureIds, viewId)
d(TAG, "LoginMethod IDs: ${data.targetLoginMethodIds}")
d(TAG, "Endpoint IDs: ${data.targetEndpointIds}")

View File

@ -38,35 +38,39 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
when (endpointId) {
ENDPOINT_VULCAN_API_DICTIONARIES -> {
data.startProgress(R.string.edziennik_progress_endpoint_dictionaries)
VulcanApiDictionaries(data) { onSuccess() }
VulcanApiDictionaries(data, onSuccess)
}
ENDPOINT_VULCAN_API_GRADES -> {
data.startProgress(R.string.edziennik_progress_endpoint_grades)
VulcanApiGrades(data) { onSuccess() }
VulcanApiGrades(data, onSuccess)
}
ENDPOINT_VULCAN_API_GRADES_SUMMARY -> {
data.startProgress(R.string.edziennik_progress_endpoint_proposed_grades)
VulcanApiProposedGrades(data) { onSuccess() }
VulcanApiProposedGrades(data, onSuccess)
}
ENDPOINT_VULCAN_API_EVENTS -> {
data.startProgress(R.string.edziennik_progress_endpoint_events)
VulcanApiEvents(data, isHomework = false) { onSuccess() }
VulcanApiEvents(data, isHomework = false, onSuccess = onSuccess)
}
ENDPOINT_VULCAN_API_HOMEWORK -> {
data.startProgress(R.string.edziennik_progress_endpoint_homework)
VulcanApiEvents(data, isHomework = true) { onSuccess() }
VulcanApiEvents(data, isHomework = true, onSuccess = onSuccess)
}
ENDPOINT_VULCAN_API_NOTICES -> {
data.startProgress(R.string.edziennik_progress_endpoint_notices)
VulcanApiNotices(data) { onSuccess() }
VulcanApiNotices(data, onSuccess)
}
ENDPOINT_VULCAN_API_ATTENDANCE -> {
data.startProgress(R.string.edziennik_progress_endpoint_attendance)
VulcanApiAttendance(data) { onSuccess() }
VulcanApiAttendance(data, onSuccess)
}
ENDPOINT_VULCAN_API_MESSAGES_INBOX -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox)
VulcanApiMessagesInbox(data) { onSuccess() }
VulcanApiMessagesInbox(data, onSuccess)
}
ENDPOINT_VULCAN_API_MESSAGES_SENT -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_outbox)
VulcanApiMessagesSent(data, onSuccess)
}
else -> onSuccess()
}

View File

@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.api.v2.vulcan.ENDPOINT_VULCAN_API_MESSAGES_INB
import pl.szczodrzynski.edziennik.api.v2.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.utils.models.Date
@ -20,64 +21,68 @@ class VulcanApiMessagesInbox(override val data: DataVulcan, val onSuccess: () ->
const val TAG = "VulcanApiMessagesInbox"
}
init { data.profile?.also { profile ->
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
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 ?: return@forEach
val addedDate = message.getLong("DataWyslaniaUnixEpoch")?.let { it * 1000 } ?: -1
val readDate = message.getLong("DataPrzeczytaniaUnixEpoch")?.let { it * 1000 } ?: -1
val messageObject = Message(
profileId,
id,
subject,
body,
Message.TYPE_RECEIVED,
senderId,
-1
)
val messageRecipientObject = MessageRecipient(
profileId,
-1,
-1,
readDate,
id
)
data.messageList.add(messageObject)
data.messageRecipientList.add(messageRecipientObject)
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_MESSAGE,
id,
readDate > 0,
readDate > 0,
addedDate
))
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
data.setSyncNext(ENDPOINT_VULCAN_API_MESSAGES_INBOX, SYNC_ALWAYS)
onSuccess()
}
} ?: onSuccess()}
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 ?: return@forEach
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,
TYPE_RECEIVED,
senderId,
-1
)
val messageRecipientObject = MessageRecipient(
profileId,
-1,
-1,
readDate,
id
)
data.messageList.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()
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-11-5
*/
package pl.szczodrzynski.edziennik.api.v2.vulcan.data.api
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES
import pl.szczodrzynski.edziennik.api.v2.VULCAN_API_ENDPOINT_MESSAGES_SENT
import pl.szczodrzynski.edziennik.api.v2.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.api.v2.vulcan.ENDPOINT_VULCAN_API_MESSAGES_SENT
import pl.szczodrzynski.edziennik.api.v2.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.utils.models.Date
class VulcanApiMessagesSent(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) {
companion object {
const val TAG = "VulcanApiMessagesSent"
}
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
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
val messageObject = Message(
profileId,
id,
subject,
body,
TYPE_SENT,
-1,
-1
)
message.getJsonArray("Adresaci")?.asJsonObjectList()
?.forEachIndexed { _, recipient ->
val recipientLoginId = recipient.getString("LoginId")
?: return@forEachIndexed
val recipientId = data.teacherList.singleOrNull { it.loginId == recipientLoginId }?.id
?: return@forEachIndexed
val readDate: Long = when (readBy) {
0 -> 0
else -> when (unreadBy) {
0 -> 1
else -> -1
}
}
val messageRecipientObject = MessageRecipient(
profileId,
recipientId,
-1,
readDate,
id
)
data.messageRecipientList.add(messageRecipientObject)
}
data.messageList.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()
}
}
}
}

View File

@ -6,8 +6,6 @@ package pl.szczodrzynski.edziennik.api.v2.vulcan.data.api
import pl.szczodrzynski.edziennik.api.v2.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.api.v2.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.getJsonArray
class VulcanApiTemplate(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) {
companion object {
@ -15,10 +13,12 @@ class VulcanApiTemplate(override val data: DataVulcan, val onSuccess: () -> Unit
}
init {
/* apiGet(TAG, VULCAN_API_ENDPOINT_) { json, _ ->
/* data.profile?.also { profile ->
apiGet(TAG, VULCAN_API_ENDPOINT_) { json, _ ->
data.setSyncNext(ENDPOINT_VULCAN_API_, SYNC_ALWAYS)
onSuccess()
data.setSyncNext(ENDPOINT_VULCAN_API_, SYNC_ALWAYS)
onSuccess()
}
} */
}
}

View File

@ -72,6 +72,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.teachers.TeacherAbsenceTypeDao
import pl.szczodrzynski.edziennik.data.db.modules.teachers.TeacherDao;
import pl.szczodrzynski.edziennik.data.db.modules.teams.Team;
import pl.szczodrzynski.edziennik.data.db.modules.teams.TeamDao;
import pl.szczodrzynski.edziennik.data.db.modules.timetable.TimetableDao;
import pl.szczodrzynski.edziennik.utils.models.Date;
@Database(entities = {
@ -103,7 +104,8 @@ import pl.szczodrzynski.edziennik.utils.models.Date;
Classroom.class,
NoticeType.class,
AttendanceType.class,
Metadata.class}, version = 63)
pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson.class,
Metadata.class}, version = 64)
@TypeConverters({
ConverterTime.class,
ConverterDate.class,
@ -141,6 +143,7 @@ public abstract class AppDb extends RoomDatabase {
public abstract ClassroomDao classroomDao();
public abstract NoticeTypeDao noticeTypeDao();
public abstract AttendanceTypeDao attendanceTypeDao();
public abstract TimetableDao timetableDao();
public abstract MetadataDao metadataDao();
private static volatile AppDb INSTANCE;
@ -729,6 +732,37 @@ public abstract class AppDb extends RoomDatabase {
database.execSQL("ALTER TABLE profiles ADD COLUMN studentSchoolYear TEXT DEFAULT NULL");
}
};
private static final Migration MIGRATION_63_64 = new Migration(63, 64) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
//database.execSQL("ALTER TABLE lessons RENAME TO lessonsOld;");
database.execSQL("CREATE TABLE timetable (" +
"profileId INTEGER NOT NULL," +
"id INTEGER NOT NULL," +
"type INTEGER NOT NULL," +
"date TEXT DEFAULT NULL," +
"lessonNumber INTEGER DEFAULT NULL," +
"startTime TEXT DEFAULT NULL," +
"endTime TEXT DEFAULT NULL," +
"subjectId INTEGER DEFAULT NULL," +
"teacherId INTEGER DEFAULT NULL," +
"teamId INTEGER DEFAULT NULL," +
"classroom TEXT DEFAULT NULL," +
"oldDate TEXT DEFAULT NULL," +
"oldLessonNumber INTEGER DEFAULT NULL," +
"oldStartTime TEXT DEFAULT NULL," +
"oldEndTime TEXT DEFAULT NULL," +
"oldSubjectId INTEGER DEFAULT NULL," +
"oldTeacherId INTEGER DEFAULT NULL," +
"oldTeamId INTEGER DEFAULT NULL," +
"oldClassroom TEXT DEFAULT NULL," +
"PRIMARY KEY(id));");
database.execSQL("CREATE INDEX index_lessons_profileId_type_date ON timetable (profileId, type, date);");
database.execSQL("CREATE INDEX index_lessons_profileId_type_oldDate ON timetable (profileId, type, oldDate);");
}
};
public static AppDb getDatabase(final Context context) {
@ -789,7 +823,8 @@ public abstract class AppDb extends RoomDatabase {
MIGRATION_59_60,
MIGRATION_60_61,
MIGRATION_61_62,
MIGRATION_62_63
MIGRATION_62_63,
MIGRATION_63_64
)
.allowMainThreadQueries()
//.fallbackToDestructiveMigration()

View File

@ -8,6 +8,7 @@ import androidx.room.Entity;
import androidx.room.Ignore;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull;
@ -73,7 +74,7 @@ public class LoginStore {
if (data == null)
return defaultValue;
JsonElement element = data.get(key);
if (element != null) {
if (element != null && !(element instanceof JsonNull)) {
return element.getAsString();
}
return defaultValue;
@ -83,7 +84,7 @@ public class LoginStore {
if (data == null)
return defaultValue;
JsonElement element = data.get(key);
if (element != null) {
if (element != null && !(element instanceof JsonNull)) {
return element.getAsInt();
}
return defaultValue;
@ -93,7 +94,7 @@ public class LoginStore {
if (data == null)
return defaultValue;
JsonElement element = data.get(key);
if (element != null) {
if (element != null && !(element instanceof JsonNull)) {
return element.getAsLong();
}
return defaultValue;
@ -103,7 +104,7 @@ public class LoginStore {
if (data == null)
return defaultValue;
JsonElement element = data.get(key);
if (element != null) {
if (element != null && !(element instanceof JsonNull)) {
return element.getAsFloat();
}
return defaultValue;
@ -112,7 +113,7 @@ public class LoginStore {
if (data == null)
return defaultValue;
JsonElement element = data.get(key);
if (element != null) {
if (element != null && !(element instanceof JsonNull)) {
return element.getAsBoolean();
}
return defaultValue;

View File

@ -4,12 +4,20 @@
package pl.szczodrzynski.edziennik.data.db.modules.timetable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
open class Lesson(val profileId: Int) {
@Entity(tableName = "timetable",
indices = [
Index(value = ["profileId", "type", "date"]),
Index(value = ["profileId", "type", "oldDate"])
])
open class Lesson(val profileId: Int, @PrimaryKey val id: Long) {
companion object {
const val TYPE_NO_LESSONS = -1
const val TYPE_NORMAL = 0
const val TYPE_CANCELLED = 1
const val TYPE_CHANGE = 2
@ -17,15 +25,14 @@ open class Lesson(val profileId: Int) {
const val TYPE_SHIFTED_TARGET = 4 /* target lesson */
}
@ColumnInfo(name = "lessonType")
var type: Int = TYPE_NORMAL
var date: Date? = null
var lessonNumber: Int? = null
var startTime: Time? = null
var endTime: Time? = null
var teacherId: Long? = null
var subjectId: Long? = null
var teacherId: Long? = null
var teamId: Long? = null
var classroom: String? = null
@ -33,8 +40,58 @@ open class Lesson(val profileId: Int) {
var oldLessonNumber: Int? = null
var oldStartTime: Time? = null
var oldEndTime: Time? = null
var oldTeacherId: Long? = null
var oldSubjectId: Long? = null
var oldTeacherId: Long? = null
var oldTeamId: Long? = null
var oldClassroom: String? = null
}
override fun toString(): String {
return "Lesson(profileId=$profileId, " +
"id=$id, " +
"type=$type, " +
"date=$date, " +
"lessonNumber=$lessonNumber, " +
"startTime=$startTime, " +
"endTime=$endTime, " +
"subjectId=$subjectId, " +
"teacherId=$teacherId, " +
"teamId=$teamId, " +
"classroom=$classroom, " +
"oldDate=$oldDate, " +
"oldLessonNumber=$oldLessonNumber, " +
"oldStartTime=$oldStartTime, " +
"oldEndTime=$oldEndTime, " +
"oldSubjectId=$oldSubjectId, " +
"oldTeacherId=$oldTeacherId, " +
"oldTeamId=$oldTeamId, " +
"oldClassroom=$oldClassroom)"
}
}
/*
DROP TABLE lessons;
DROP TABLE lessonChanges;
CREATE TABLE lessons (
profileId INTEGER NOT NULL,
type INTEGER NOT NULL,
date TEXT DEFAULT NULL,
lessonNumber INTEGER DEFAULT NULL,
startTime TEXT DEFAULT NULL,
endTime TEXT DEFAULT NULL,
teacherId INTEGER DEFAULT NULL,
subjectId INTEGER DEFAULT NULL,
teamId INTEGER DEFAULT NULL,
classroom TEXT DEFAULT NULL,
oldDate TEXT DEFAULT NULL,
oldLessonNumber INTEGER DEFAULT NULL,
oldStartTime TEXT DEFAULT NULL,
oldEndTime TEXT DEFAULT NULL,
oldTeacherId INTEGER DEFAULT NULL,
oldSubjectId INTEGER DEFAULT NULL,
oldTeamId INTEGER DEFAULT NULL,
oldClassroom TEXT DEFAULT NULL,
PRIMARY KEY(profileId)
);
*/

View File

@ -0,0 +1,69 @@
package pl.szczodrzynski.edziennik.data.db.modules.timetable
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
class LessonFull(profileId: Int, id: Long) : Lesson(profileId, id) {
var subjectName: String? = null
var teacherName: String? = null
var teamName: String? = null
var oldSubjectName: String? = null
var oldTeacherName: String? = null
var oldTeamName: String? = null
val displayDate: Date?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldDate
return date ?: oldDate
}
val displayLessonNumber: Int?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldLessonNumber
return lessonNumber ?: oldLessonNumber
}
val displayStartTime: Time?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldStartTime
return startTime ?: oldStartTime
}
val displayEndTime: Time?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldEndTime
return endTime ?: oldEndTime
}
val displaySubjectName: String?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldSubjectName
return subjectName ?: oldSubjectName
}
val displayTeacherName: String?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldTeacherName
return teacherName ?: oldTeacherName
}
val displayTeamName: String?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldTeamName
return teamName ?: oldTeamName
}
val displayClassroom: String?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldClassroom
return classroom ?: oldClassroom
}
// metadata
var seen: Boolean = false
var notified: Boolean = false
var addedDate: Long = 0
}

View File

@ -0,0 +1,49 @@
package pl.szczodrzynski.edziennik.data.db.modules.timetable
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.utils.models.Date
@Dao
interface TimetableDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
operator fun plusAssign(lessonList: List<Lesson>)
@Query("DELETE FROM timetable WHERE profileId = :profileId")
fun clear(profileId: Int)
@Query("DELETE FROM timetable WHERE profileId = :profileId AND (type != 3 AND date >= :dateFrom) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom)")
fun clearFromDate(profileId: Int, dateFrom: Date)
@Query("DELETE FROM timetable WHERE profileId = :profileId AND (type != 3 AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate <= :dateTo)")
fun clearToDate(profileId: Int, dateTo: Date)
@Query("DELETE FROM timetable WHERE profileId = :profileId AND (type != 3 AND date >= :dateFrom AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom AND oldDate <= :dateTo)")
fun clearBetweenDates(profileId: Int, dateFrom: Date, dateTo: Date)
@Query("""
SELECT
timetable.*,
subjects.subjectLongName AS subjectName,
teachers.teacherName ||" "|| teachers.teacherSurname AS teacherName,
teams.teamName AS teamName,
oldS.subjectLongName AS oldSubjectName,
oldT.teacherName ||" "|| oldT.teacherSurname AS oldTeacherName,
oldG.teamName AS oldTeamName,
metadata.seen, metadata.notified, metadata.addedDate
FROM timetable
LEFT JOIN subjects USING(profileId, subjectId)
LEFT JOIN teachers USING(profileId, teacherId)
LEFT JOIN teams USING(profileId, teamId)
LEFT JOIN subjects AS oldS ON timetable.profileId = oldS.profileId AND timetable.oldSubjectId = oldS.subjectId
LEFT JOIN teachers AS oldT ON timetable.profileId = oldT.profileId AND timetable.oldTeacherId = oldT.teacherId
LEFT JOIN teams AS oldG ON timetable.profileId = oldG.profileId AND timetable.oldTeamId = oldG.teamId
LEFT JOIN metadata ON id = thingId AND thingType = ${Metadata.TYPE_LESSON_CHANGE} AND metadata.profileId = timetable.profileId
WHERE timetable.profileId = :profileId AND (type != 3 AND date = :date) OR ((type = 3 OR type = 1) AND oldDate = :date)
ORDER BY type
""")
fun getForDate(profileId: Int, date: Date) : LiveData<List<LessonFull>>
}

View File

@ -112,7 +112,7 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
app.notifier.postAll(profile);
app.saveConfig("notifications");*/
d(TAG, "Syncing profile " + profile.getId());
EdziennikTask.Companion.syncProfile(profile.getId(), null).enqueue(app);
EdziennikTask.Companion.syncProfile(profile.getId(), null, null).enqueue(app);
} else {
/*app.notifier.add(new Notification(app.getContext(), remoteMessage.getData().get("message"))
.withProfileData(profile.id, profile.name)
@ -123,7 +123,7 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
app.notifier.postAll(profile);
app.saveConfig("notifications");*/
d(TAG, "Syncing profile " + profile.getId());
EdziennikTask.Companion.syncProfile(profile.getId(), null).enqueue(app);
EdziennikTask.Companion.syncProfile(profile.getId(), null, null).enqueue(app);
}
}
});

View File

@ -6,6 +6,7 @@ import android.content.Intent;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
@ -30,11 +31,14 @@ import androidx.fragment.app.Fragment;
import androidx.work.WorkManager;
import com.afollestad.materialdialogs.MaterialDialog;
import com.chuckerteam.chucker.api.Chucker;
import com.hypertrack.hyperlog.HyperLog;
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.io.File;
import java.util.ArrayList;
import java.util.List;
@ -50,6 +54,7 @@ import pl.szczodrzynski.edziennik.databinding.CardLuckyNumberBinding;
import pl.szczodrzynski.edziennik.databinding.CardUpdateBinding;
import pl.szczodrzynski.edziennik.databinding.FragmentHomeBinding;
import pl.szczodrzynski.edziennik.receivers.BootReceiver;
import pl.szczodrzynski.edziennik.ui.modules.login.LoginLibrusCaptchaActivity;
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesComposeActivity;
import pl.szczodrzynski.edziennik.utils.Colors;
import pl.szczodrzynski.edziennik.utils.Themes;
@ -104,6 +109,27 @@ public class HomeFragment extends Fragment {
b.pruneWorkButton.setOnClickListener((v -> WorkManager.getInstance(app).pruneWork()));
b.runChucker.setOnClickListener((v -> {
startActivity(Chucker.getLaunchIntent(activity, 1));
}));
b.librusCaptchaButton.setOnClickListener((v -> {
startActivity(new Intent(activity, LoginLibrusCaptchaActivity.class));
}));
b.getLogs.setOnClickListener((v -> {
File logs = HyperLog.getDeviceLogsInFile(activity, true);
Intent intent = new Intent(Intent.ACTION_SEND);
if(logs.exists()) {
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://"+logs.getAbsolutePath()));
intent.putExtra(Intent.EXTRA_SUBJECT, "Share debug logs");
intent.putExtra(Intent.EXTRA_TEXT, "Share debug logs");
startActivity(Intent.createChooser(intent, "Share debug logs"));
}
}));
//((TextView)v.findViewById(R.id.nextSync)).setText(getString(R.string.next_sync_format,Time.fromMillis(app.appJobs.syncJobTime).getStringHMS()));

View File

@ -26,6 +26,7 @@ public class LoginChooserFragment extends Fragment {
private NavController nav;
private FragmentLoginChooserBinding b;
private static final String TAG = "LoginTemplate";
public static boolean fakeLogin = false;
public LoginChooserFragment() { }
@ -71,6 +72,10 @@ public class LoginChooserFragment extends Fragment {
b.cancelButton.setVisibility(View.GONE);
}
b.fakeLogin.setVisibility(App.devMode ? View.VISIBLE : View.GONE);
b.fakeLogin.setChecked(fakeLogin);
b.fakeLogin.setOnCheckedChangeListener((v, isChecked) -> fakeLogin = isChecked);
b.helpButton.setOnClickListener((v -> {
startActivity(new Intent(getActivity(), FeedbackActivity.class));
}));

View File

@ -0,0 +1,116 @@
package pl.szczodrzynski.edziennik.ui.modules.login
import android.annotation.SuppressLint
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.util.Base64
import android.webkit.JavascriptInterface
import android.webkit.WebView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.afollestad.materialdialogs.MaterialDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.api.v2.LIBRUS_USER_AGENT
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.Utils.hexFromColorInt
import java.nio.charset.Charset
class LoginLibrusCaptchaActivity : AppCompatActivity() {
companion object {
private const val TAG = "LoginLibrusCaptchaActivity"
}
private lateinit var webView: WebView
private lateinit var dialog: AlertDialog
private lateinit var jsInterface: CaptchaCallbackInterface
@SuppressLint("AddJavascriptInterface", "SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTheme(Themes.appThemeNoDisplay)
setFinishOnTouchOutside(false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true)
}
val base64Content = """
PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PHNjcmlwdCBzcmM9Imh0dHBzOi8vd3d3Lmdvb2ds
ZS5jb20vcmVjYXB0Y2hhL2FwaS5qcz9vbmxvYWQ9cmVhZHkmcmVuZGVyPWV4cGxpY2l0Ij48L3Nj
cmlwdD48L2hlYWQ+PGJvZHk+PGJyPjxjZW50ZXIgaWQ9ImdyIj48L2NlbnRlcj48YnI+PHNjcmlw
dD5mdW5jdGlvbiByZWFkeSgpe2dyZWNhcHRjaGEucmVuZGVyKCdncicse3NpdGVrZXk6JzZMZjQ4
bW9VQUFBQUFCOUNsaGR2SHI0NmdSV1ItQ04zMUNYUVBHMlUnLHRoZW1lOidUSEVNRScsY2FsbGJh
Y2s6ZnVuY3Rpb24oZSl7d2luZG93LmlmLmNhbGxiYWNrKGUpO30sImV4cGlyZWQtY2FsbGJhY2si
OmZ1bmN0aW9uKCl7d2luZG93LmlmLmV4cGlyZWRDYWxsYmFjayhlKTt9LCJlcnJvci1jYWxsYmFj
ayI6ZnVuY3Rpb24oKXt3aW5kb3cuaWYuZXJyb3JDYWxsYmFjayhlKTt9fSk7fTwvc2NyaXB0Pjwv
Ym9keT48L2h0bWw+"""
val backgroundColor = if (Themes.isDark) 0x424242 else 0xffffff
val backgroundColorString = hexFromColorInt(backgroundColor)
val htmlContent = Base64.decode(base64Content, Base64.DEFAULT)
.toString(Charset.defaultCharset())
.replace("COLOR", backgroundColorString, true)
.replace("THEME", if (Themes.isDark) "dark" else "light")
jsInterface = object : CaptchaCallbackInterface {
@JavascriptInterface
override fun callback(recaptchaResponse: String) {
MaterialDialog.Builder(this@LoginLibrusCaptchaActivity)
.title("Captcha checked")
.content("Response: $recaptchaResponse")
.positiveText("OK")
.show()
}
@JavascriptInterface
override fun expiredCallback() {
MaterialDialog.Builder(this@LoginLibrusCaptchaActivity)
.title("Captcha expired")
.content("Captcha expired")
.positiveText("OK")
.show()
}
@JavascriptInterface
override fun errorCallback() {
MaterialDialog.Builder(this@LoginLibrusCaptchaActivity)
.title("Captcha error")
.content("Captcha error")
.positiveText("OK")
.show()
}
}
webView = WebView(this).apply {
//setBackgroundColor((backgroundColor.toLong() or 0xff000000).toInt())
setBackgroundColor(Color.TRANSPARENT)
settings.javaScriptEnabled = true
settings.userAgentString = LIBRUS_USER_AGENT
addJavascriptInterface(jsInterface, "if")
loadDataWithBaseURL("https://portal.librus.pl/rodzina/login/", htmlContent, "text/html", "UTF-8", null)
setLayerType(WebView.LAYER_TYPE_SOFTWARE, null)
}
dialog = MaterialAlertDialogBuilder(this)
.setTitle(R.string.login_librus_captcha_title)
.setView(webView)
.setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
finish()
}
.setCancelable(false)
.show()
}
interface CaptchaCallbackInterface {
@JavascriptInterface
fun callback(recaptchaResponse: String)
@JavascriptInterface
fun expiredCallback()
@JavascriptInterface
fun errorCallback()
}
}

View File

@ -89,6 +89,10 @@ public class LoginProgressFragment extends Fragment {
LoginStore loginStore = new LoginStore(-1, loginType, new JsonObject());
loginStore.copyFrom(args);
if (App.devMode && LoginChooserFragment.fakeLogin) {
loginStore.putLoginData("fakeLogin", true);
}
EdziennikTask.Companion.firstLogin(loginStore).enqueue(getContext());
}

View File

@ -533,7 +533,7 @@ public class SettingsNewFragment extends MaterialAboutFragment {
syncCardIntervalItem.setChecked(app.appConfig.registerSyncEnabled);
syncCardIntervalItem.setOnClickAction(() -> {
List<CharSequence> intervalNames = new ArrayList<>();
if (App.devMode) {
if (App.devMode && false) {
intervalNames.add(HomeFragment.plural(activity, R.plurals.time_till_seconds, 30));
intervalNames.add(HomeFragment.plural(activity, R.plurals.time_till_minutes, 2));
}
@ -545,7 +545,7 @@ public class SettingsNewFragment extends MaterialAboutFragment {
intervalNames.add(HomeFragment.plural(activity, R.plurals.time_till_hours, 3));
intervalNames.add(HomeFragment.plural(activity, R.plurals.time_till_hours, 4));
List<Integer> intervals = new ArrayList<>();
if (App.devMode) {
if (App.devMode && false) {
intervals.add(30);
intervals.add(2 * 60);
}

View File

@ -0,0 +1,108 @@
package pl.szczodrzynski.edziennik.ui.modules.timetable.v2
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewpager.widget.ViewPager
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.api.v2.LOGIN_TYPE_LIBRUS
import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.models.Date
class TimetableFragment : Fragment() {
companion object {
private const val TAG = "TimetableFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: FragmentTimetableV2Binding
private var fabShown = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
if (context == null)
return null
app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true)
if (app.profile == null)
return inflater.inflate(R.layout.fragment_loading, container, false)
// activity, context and profile is valid
b = FragmentTimetableV2Binding.inflate(inflater)
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
return
if (app.profile.loginStoreType == LOGIN_TYPE_LIBRUS && app.profile.getLoginData("timetableNotPublic", false)) {
b.timetableLayout.visibility = View.GONE
b.timetableNotPublicLayout.visibility = View.VISIBLE
return
}
b.timetableLayout.visibility = View.VISIBLE
b.timetableNotPublicLayout.visibility = View.GONE
val items = mutableListOf<Date>()
val monthDayCount = listOf(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
val today = Date.getToday().value
val yearStart = app.profile.dateSemester1Start?.clone() ?: return
val yearEnd = app.profile.dateYearEnd ?: return
while (yearStart.value <= yearEnd.value) {
items += yearStart.clone()
var maxDays = monthDayCount[yearStart.month-1]
if (yearStart.month == 2 && yearStart.isLeap)
maxDays++
yearStart.day++
if (yearStart.day > maxDays) {
yearStart.day = 1
yearStart.month++
}
if (yearStart.month > 12) {
yearStart.month = 1
yearStart.year++
}
}
val pagerAdapter = TimetablePagerAdapter(fragmentManager ?: return, items)
b.viewPager.offscreenPageLimit = 2
b.viewPager.adapter = pagerAdapter
b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
}
override fun onPageSelected(position: Int) {
activity.navView.bottomBar.fabEnable = items[position].value != today
if (activity.navView.bottomBar.fabEnable && !fabShown) {
activity.gainAttentionFAB()
fabShown = true
}
}
})
b.tabLayout.setUpWithViewPager(b.viewPager)
b.tabLayout.setCurrentItem(items.indexOfFirst { it.value == today }, false)
//activity.navView.bottomBar.fabEnable = true
activity.navView.bottomBar.fabExtendedText = getString(R.string.timetable_today)
activity.navView.bottomBar.fabIcon = CommunityMaterial.Icon.cmd_calendar_today
activity.navView.setFabOnClickListener(View.OnClickListener {
b.tabLayout.setCurrentItem(items.indexOfFirst { it.value == today }, true)
})
}
}

View File

@ -0,0 +1,39 @@
package pl.szczodrzynski.edziennik.ui.modules.timetable.v2
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Week
class TimetablePagerAdapter(val fragmentManager: FragmentManager, val items: List<Date>) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
companion object {
private const val TAG = "TimetablePagerAdapter"
}
private val today by lazy { Date.getToday() }
private val weekStart by lazy { today.clone().stepForward(0, 0, -today.weekDay) }
private val weekEnd by lazy { weekStart.clone().stepForward(0, 0, 6) }
override fun getItem(position: Int): Fragment {
return pl.szczodrzynski.edziennik.ui.modules.timetable.v2.day.TimetableDayFragment(items[position])
/*return TimetableDayFragment().apply {
arguments = Bundle().also {
it.putLong("date", items[position].value.toLong())
}
}*/
}
override fun getCount(): Int {
return items.size
}
override fun getPageTitle(position: Int): CharSequence? {
val date = items[position]
val pageTitle = StringBuilder(Week.getFullDayName(date.weekDay))
if (date > weekEnd || date < weekStart) {
pageTitle.append(", ").append(date.stringDm)
}
return pageTitle
}
}

View File

@ -0,0 +1,285 @@
package pl.szczodrzynski.edziennik.ui.modules.timetable.v2.day
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import com.linkedin.android.tachyon.DayView
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE
import pl.szczodrzynski.edziennik.api.v2.LOGIN_TYPE_LIBRUS
import pl.szczodrzynski.edziennik.api.v2.events.task.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson
import pl.szczodrzynski.edziennik.data.db.modules.timetable.LessonFull
import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2DayBinding
import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.getColorFromAttr
import java.util.*
import kotlin.math.min
class TimetableDayFragment(val date: Date) : Fragment() {
companion object {
private const val TAG = "TimetableDayFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: FragmentTimetableV2DayBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
if (context == null)
return null
app = activity.application as App
b = FragmentTimetableV2DayBinding.inflate(inflater)
Log.d(TAG, "onCreateView, date=$date")
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
return
Log.d(TAG, "onViewCreated, date=$date")
// Inflate a label view for each hour the day view will display
val hourLabelViews = ArrayList<View>()
for (i in b.day.startHour..b.day.endHour) {
val hourLabelView = layoutInflater.inflate(R.layout.timetable_hour_label, b.day, false) as TextView
hourLabelView.text = "$i:00"
hourLabelViews.add(hourLabelView)
}
b.day.setHourLabelViews(hourLabelViews)
app.db.timetableDao().getForDate(App.profileId, date).observe(this, Observer<List<LessonFull>> { lessons ->
buildLessonViews(lessons)
})
}
private fun buildLessonViews(lessons: List<LessonFull>) {
if (lessons.isEmpty()) {
b.dayScroll.visibility = View.GONE
b.noTimetableLayout.visibility = View.VISIBLE
b.noLessonsLayout.visibility = View.GONE
val weekStart = date.clone().stepForward(0, 0, -date.weekDay).stringY_m_d
b.noTimetableSync.setOnClickListener {
EdziennikTask.syncProfile(
profileId = App.profileId,
viewIds = listOf(
DRAWER_ITEM_TIMETABLE to 0
),
arguments = JsonObject(
"weekStart" to weekStart
)
).enqueue(activity)
}
b.noTimetableWeek.setText(R.string.timetable_no_timetable_week, weekStart)
return
}
if (lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) {
b.dayScroll.visibility = View.GONE
b.noTimetableLayout.visibility = View.GONE
b.noLessonsLayout.visibility = View.VISIBLE
return
}
// reload the fragment when: no lessons, user wants to sync the week, the timetable is not public, pager gets removed
if (app.profile.loginStoreType == LOGIN_TYPE_LIBRUS && app.profile.getLoginData("timetableNotPublic", false)) {
activity.reloadTarget()
// TODO fix for (not really)possible infinite loops
return
}
b.dayScroll.visibility = View.VISIBLE
b.noTimetableLayout.visibility = View.GONE
b.noLessonsLayout.visibility = View.GONE
var firstEventMinute = 24*60
val eventViews = mutableListOf<View>()
val eventTimeRanges = mutableListOf<DayView.EventTimeRange>()
// Reclaim all of the existing event views so we can reuse them if needed, this process
// can be useful if your day view is hosted in a recycler view for example
val recycled = b.day.removeEventViews()
var remaining = recycled?.size ?: 0
val arrowRight = ""
val bullet = ""
val colorSecondary = getColorFromAttr(activity, android.R.attr.textColorSecondary)
for (lesson in lessons) {
val startTime = lesson.displayStartTime ?: continue
val endTime = lesson.displayEndTime ?: continue
firstEventMinute = min(firstEventMinute, startTime.hour*60 + startTime.minute)
// Try to recycle an existing event view if there are enough left, otherwise inflate
// a new one
val eventView = (if (remaining > 0) recycled?.get(--remaining) else layoutInflater.inflate(R.layout.timetable_lesson, b.day, false))
?: continue
val lb = TimetableLessonBinding.bind(eventView)
eventViews += eventView
eventView.tag = lesson
eventView.setOnClickListener {
Log.d(TAG, "Clicked ${it.tag}")
}
val timeRange = "${startTime.stringHM} - ${endTime.stringHM}".asColoredSpannable(colorSecondary)
// teacher
val teacherInfo = if (lesson.teacherId != null && lesson.teacherId == lesson.oldTeacherId)
lesson.teacherName ?: "?"
else
mutableListOf<CharSequence>().apply {
lesson.oldTeacherName?.let { add(it.asStrikethroughSpannable()) }
lesson.teacherName?.let { add(it) }
}.concat(arrowRight)
// team
val teamInfo = if (lesson.teamId != null && lesson.teamId == lesson.oldTeamId)
lesson.teamName ?: "?"
else
mutableListOf<CharSequence>().apply {
lesson.oldTeamName?.let { add(it.asStrikethroughSpannable()) }
lesson.teamName?.let { add(it) }
}.concat(arrowRight)
// classroom
val classroomInfo = if (lesson.classroom != null && lesson.classroom == lesson.oldClassroom)
lesson.classroom ?: "?"
else
mutableListOf<CharSequence>().apply {
lesson.oldClassroom?.let { add(it.asStrikethroughSpannable()) }
lesson.classroom?.let { add(it) }
}.concat(arrowRight)
lb.lessonNumber = lesson.displayLessonNumber
lb.subjectName.text = lesson.displaySubjectName?.let {
if (lesson.type == Lesson.TYPE_CANCELLED || lesson.type == Lesson.TYPE_SHIFTED_SOURCE)
it.asStrikethroughSpannable().asColoredSpannable(colorSecondary)
else
it
}
lb.detailsFirst.text = listOfNotEmpty(timeRange, classroomInfo).concat(bullet)
lb.detailsSecond.text = listOfNotEmpty(teacherInfo, teamInfo).concat(bullet)
//lb.subjectName.typeface = Typeface.create("sans-serif-light", Typeface.BOLD)
when (lesson.type) {
Lesson.TYPE_NORMAL -> {
lb.annotationVisible = false
}
Lesson.TYPE_CANCELLED -> {
lb.annotationVisible = true
lb.annotation.setText(R.string.timetable_lesson_cancelled)
lb.annotation.background.colorFilter = PorterDuffColorFilter(
getColorFromAttr(activity, R.attr.timetable_lesson_cancelled_color),
PorterDuff.Mode.SRC_ATOP
)
//lb.subjectName.typeface = Typeface.DEFAULT
}
Lesson.TYPE_CHANGE -> {
lb.annotationVisible = true
if (lesson.subjectId != lesson.oldSubjectId && lesson.teacherId != lesson.oldTeacherId) {
lb.annotation.setText(
R.string.timetable_lesson_change_format,
"${lesson.oldSubjectName ?: "?"}, ${lesson.oldTeacherName ?: "?"}"
)
}
else if (lesson.subjectId != lesson.oldSubjectId) {
lb.annotation.setText(
R.string.timetable_lesson_change_format,
lesson.oldSubjectName ?: "?"
)
}
else if (lesson.teacherId != lesson.oldTeacherId) {
lb.annotation.setText(
R.string.timetable_lesson_change_format,
lesson.oldTeacherName ?: "?"
)
}
else {
lb.annotation.setText(R.string.timetable_lesson_change)
}
lb.annotation.background.colorFilter = PorterDuffColorFilter(
getColorFromAttr(activity, R.attr.timetable_lesson_change_color),
PorterDuff.Mode.SRC_ATOP
)
}
Lesson.TYPE_SHIFTED_SOURCE -> {
lb.annotationVisible = true
if (lesson.date != lesson.oldDate) {
lb.annotation.setText(
R.string.timetable_lesson_shifted_other_day,
lesson.date?.stringY_m_d ?: "?",
lesson.startTime?.stringHM ?: "?"
)
}
else if (lesson.startTime != lesson.oldStartTime) {
lb.annotation.setText(
R.string.timetable_lesson_shifted_same_day,
lesson.startTime?.stringHM ?: "?"
)
}
else {
lb.annotation.setText(R.string.timetable_lesson_shifted)
}
lb.annotation.background.colorFilter = PorterDuffColorFilter(
getColorFromAttr(activity, R.attr.timetable_lesson_shifted_source_color),
PorterDuff.Mode.SRC_ATOP
)
}
Lesson.TYPE_SHIFTED_TARGET -> {
lb.annotationVisible = true
if (lesson.date != lesson.oldDate) {
lb.annotation.setText(
R.string.timetable_lesson_shifted_from_other_day,
lesson.oldDate?.stringY_m_d ?: "?",
lesson.oldStartTime?.stringHM ?: "?"
)
}
else if (lesson.startTime != lesson.oldStartTime) {
lb.annotation.setText(
R.string.timetable_lesson_shifted_from_same_day,
lesson.oldStartTime?.stringHM ?: "?"
)
}
else {
lb.annotation.setText(R.string.timetable_lesson_shifted_from)
}
lb.annotation.background.colorFilter = PorterDuffColorFilter(
getColorFromAttr(activity, R.attr.timetable_lesson_shifted_target_color),
PorterDuff.Mode.SRC_ATOP
)
}
}
// The day view needs the event time ranges in the start minute/end minute format,
// so calculate those here
val startMinute = 60 * (lesson.displayStartTime?.hour ?: 0) + (lesson.displayStartTime?.minute ?: 0)
val endMinute = startMinute + 45
eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute))
}
val minuteHeight = (b.day.getHourTop(1) - b.day.getHourTop(0)).toFloat() / 60f
val firstEventTop = (firstEventMinute - b.day.startHour * 60) * minuteHeight
b.day.setEventViews(eventViews, eventTimeRanges)
b.dayScroll.scrollTo(0, firstEventTop.toInt())
}
}

View File

@ -0,0 +1,24 @@
package pl.szczodrzynski.edziennik.utils
import android.content.ComponentName
import android.content.Intent
val appManagerIntentList = listOf(
Intent().setClassName("com.miui.powerkeeper", "com.miui.powerkeeper.ui.HiddenAppsConfigActivity")
.putExtra("packageName", "pl.szczodrzynski.edziennik")
.putExtra("package_label", "Szkolny.eu"),
Intent().setComponent(ComponentName("com.miui.securitycenter", "com.miui.permcenter.autostart.AutoStartManagementActivity")),
Intent().setComponent(ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.AutobootManageActivity")),
Intent().setComponent(ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.optimize.process.ProtectActivity")),
Intent().setComponent(ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.appcontrol.activity.StartupAppControlActivity")),
Intent().setComponent(ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity")),
Intent().setComponent(ComponentName("com.coloros.safecenter", "com.coloros.safecenter.permission.startup.StartupAppListActivity")),
Intent().setComponent(ComponentName("com.coloros.safecenter", "com.coloros.safecenter.startupapp.StartupAppListActivity")),
Intent().setComponent(ComponentName("com.oppo.safe", "com.oppo.safe.permission.startup.StartupAppListActivity")),
Intent().setComponent(ComponentName("com.iqoo.secure", "com.iqoo.secure.ui.phoneoptimize.AddWhiteListActivity")),
Intent().setComponent(ComponentName("com.iqoo.secure", "com.iqoo.secure.ui.phoneoptimize.BgStartUpManager")),
Intent().setComponent(ComponentName("com.vivo.permissionmanager", "com.vivo.permissionmanager.activity.BgStartUpManagerActivity")),
Intent().setComponent(ComponentName("com.samsung.android.lool", "com.samsung.android.sm.ui.battery.BatteryActivity")),
Intent().setComponent(ComponentName("com.htc.pitroad", "com.htc.pitroad.landingpage.activity.LandingPageActivity")),
Intent().setComponent(ComponentName("com.asus.mobilemanager", "com.asus.mobilemanager.MainActivity"))
)

View File

@ -0,0 +1,10 @@
package pl.szczodrzynski.edziennik.utils
import android.content.Context
import com.hypertrack.hyperlog.LogFormat
class DebugLogFormat(context: Context) : LogFormat(context) {
override fun getFormattedLogMessage(logLevelName: String?, tag: String?, message: String?, timeStamp: String?, senderName: String?, osVersion: String?, deviceUUID: String?): String {
return "${timeStamp?.replace("[TZ]".toRegex(), " ")}D/$tag: $message"
}
}

View File

@ -26,6 +26,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;
import com.hypertrack.hyperlog.HyperLog;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
@ -106,15 +108,15 @@ public class Utils {
public static void d(String TAG, String message) {
if (App.devMode) {
Log.d("Szkolny/"+TAG, message);
HyperLog.d("Szkolny/"+TAG, message);
//debugLog.add(TAG+": "+message);
}
}
public static void c(String TAG, String message) {
if (App.devMode) {
/*if (App.devMode) {
Log.d(TAG, "// " + message);
///debugLog.add(TAG+": // "+message);
}
}*/
}
/**

View File

@ -1,6 +1,7 @@
package pl.szczodrzynski.edziennik.utils.models;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.text.DateFormat;
import java.util.Calendar;
@ -131,6 +132,13 @@ public class Date implements Comparable<Date> {
return year * 10000 + month * 100 + day;
}
public static Date fromValue(int value) {
int year = value / 10000;
int month = (value-year*10000) / 100;
int day = (value-year*10000-month*100);
return new Date(year, month, day);
}
public String getStringValue()
{
return Integer.toString(getValue());
@ -182,6 +190,10 @@ public class Date implements Comparable<Date> {
}
}
public boolean isLeap() {
return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0);
}
public static Date getToday()
{
Calendar cal = Calendar.getInstance();
@ -214,6 +226,11 @@ public class Date implements Comparable<Date> {
return this.getValue() - o.getValue();
}
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof Date && this.getValue() == ((Date) obj).getValue();
}
@Override
public String toString() {
return "Date{" +

View File

@ -1,5 +1,7 @@
package pl.szczodrzynski.edziennik.utils.models;
import androidx.annotation.Nullable;
import java.util.Calendar;
public class Time {
@ -173,6 +175,11 @@ public class Time {
return (currentTime.getValue() >= startTime.getValue() && currentTime.getValue() <= endTime.getValue());
}
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof Time && this.getValue() == ((Time) obj).getValue();
}
@Override
public String toString() {
return "Time{" +

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:attr/colorControlHighlight">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="#000000" />
<corners android:radius="4dp" />
</shape>
</item>
</ripple>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/bg_rounded_edittext_pressed" />
<item android:state_focused="true" android:drawable="@drawable/bg_rounded_edittext_pressed" />
<item android:state_selected="true" android:drawable="@drawable/bg_rounded_edittext_pressed" />
</selector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="4dp" />
<stroke android:color="@color/dividerColor" android:width="1dp" />
<solid android:color="#DCDCDC" />
</shape>

View File

@ -0,0 +1,61 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-11.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:pathData="m117,76h4c3.852,0 7,-3.148 7,-7 0,-3.852 -3.148,-7 -7,-7h-13c-2.199,0 -4,-1.801 -4,-4s1.801,-4 4,-4h11c3.852,0 7,-3.148 7,-7 0,-3.852 -3.148,-7 -7,-7h-1c-2.211,0 -4,-1.789 -4,-4 0,-2.211 1.789,-4 4,-4h4c3.695,0 6.637,-3.387 5.879,-7.211 -0.566,-2.844 -3.238,-4.789 -6.141,-4.789h-15.738c-1.656,0 -3,-1.344 -3,-3s1.344,-3 3,-3h6.824c2.277,0 4.402,-1.441 4.992,-3.641 0.895,-3.328 -1.625,-6.359 -4.816,-6.359h-96c-3.852,0 -7,3.148 -7,7 0,3.852 3.148,7 7,7h3c2.211,0 4,1.789 4,4 0,2.211 -1.789,4 -4,4h-10.77c-3.34,0 -6.391,2.242 -7.074,5.516 -0.941,4.488 2.508,8.484 6.844,8.484h15l-6,24h-10.77c-3.34,0 -6.391,2.242 -7.074,5.516 -0.941,4.488 2.508,8.484 6.844,8.484h1c2.211,0 4,1.789 4,4 0,2.211 -1.789,4 -4,4h-1c-4.336,0 -7.785,3.996 -6.844,8.484 0.684,3.273 3.734,5.516 7.074,5.516h10.77c2.211,0 4,1.789 4,4s-1.789,4 -4,4h-2.77c-3.34,0 -6.391,2.242 -7.074,5.516 -0.941,4.488 2.508,8.484 6.844,8.484h97c3.313,0 6,-2.688 6,-6s-2.688,-6 -6,-6h-1c-2.762,0 -5,-2.238 -5,-5s2.238,-5 5,-5h6c3.852,0 7,-3.148 7,-7 0,-3.852 -3.148,-7 -7,-7 -2.75,0 -5,-2.25 -5,-5s2.25,-5 5,-5z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="100.522"
android:centerX="63.746"
android:centerY="71.138"
android:type="radial"
android:tileMode="mirror">
<item android:offset="0" android:color="#1F0FCCFF"/>
<item android:offset="0.1927" android:color="#1F0FCEFF"/>
<item android:offset="0.7025" android:color="#1F0FD5FF"/>
<item android:offset="1" android:color="#1F0FD7FF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m103.24,15.168 l-7.883,7.871c-8.746,-7.063 -19.719,-11.039 -31.355,-11.039 -27.57,0 -50,22.43 -50,50 0,27.57 22.43,50 50,50 17.758,0 34.348,-9.367 43.301,-24.449 1.41,-2.375 0.625,-5.441 -1.75,-6.852 -2.367,-1.41 -5.438,-0.629 -6.852,1.746 -7.156,12.062 -20.453,19.555 -34.699,19.555 -22.055,0 -40,-17.945 -40,-40 0,-22.055 17.945,-40 40,-40 8.934,0 17.371,2.938 24.223,8.16l-9.063,9.047c-2.48,2.5 -0.719,6.762 2.801,6.762h24.039c2.199,0 4,-1.801 4,-4v-24c0,-3.52 -4.262,-5.301 -6.762,-2.801z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="125.452"
android:startX="62"
android:endY="6.4762"
android:endX="62"
android:type="linear"
android:tileMode="mirror">
<item android:offset="0" android:color="#FFFEAA53"/>
<item android:offset="0.6124" android:color="#FFFFCD49"/>
<item android:offset="1" android:color="#FFFFDE44"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m68,85c0,2.762 -2.238,5 -5,5s-5,-2.238 -5,-5 2.238,-5 5,-5 5,2.238 5,5zM70,41c0,-3.867 -3.133,-7 -7,-7 -3.867,0 -7,3.133 -7,7 0,0.047 0.016,0.094 0.016,0.141h-0.016l2.438,28.59c0.203,2.414 2.188,4.269 4.563,4.269s4.359,-1.855 4.563,-4.269l2.438,-28.59h-0.016c0,-0.047 0.016,-0.094 0.016,-0.141z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="90"
android:startX="63"
android:endY="11.7176"
android:endX="63"
android:type="linear"
android:tileMode="mirror">
<item android:offset="0" android:color="#FFFF634D"/>
<item android:offset="0.2043" android:color="#FFFE6464"/>
<item android:offset="0.5209" android:color="#FFFC6581"/>
<item android:offset="0.7936" android:color="#FFFA6694"/>
<item android:offset="0.9892" android:color="#FFFA669A"/>
</gradient>
</aapt:attr>
</path>
</vector>

View File

@ -0,0 +1,45 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-11.
-->
<vector android:height="128dp" android:viewportHeight="64"
android:viewportWidth="64" android:width="128dp"
xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:pathData="m59,16h2c1.848,0 3.319,-1.693 2.94,-3.605 -0.283,-1.423 -1.62,-2.395 -3.071,-2.395h-10.369c-0.828,0 -1.5,-0.672 -1.5,-1.5s0.672,-1.5 1.5,-1.5h5.912c1.139,0 2.202,-0.721 2.497,-1.821 0.446,-1.663 -0.813,-3.179 -2.409,-3.179h-48c-1.925,0 -3.5,1.575 -3.5,3.5s1.575,3.5 3.5,3.5h1.5c1.105,0 2,0.895 2,2s-0.895,2 -2,2h-5.385c-1.67,0 -3.195,1.122 -3.537,2.757 -0.47,2.245 1.254,4.243 3.422,4.243h7.5l-3,12h-5.385c-1.67,0 -3.195,1.122 -3.537,2.757 -0.47,2.245 1.254,4.243 3.422,4.243h0.5c1.105,0 2,0.895 2,2s-0.895,2 -2,2h-0.5c-2.168,0 -3.892,1.998 -3.422,4.243 0.342,1.635 1.867,2.757 3.537,2.757h5.385c1.105,0 2,0.895 2,2s-0.895,2 -2,2h-1.385c-1.67,0 -3.195,1.122 -3.537,2.757 -0.47,2.245 1.254,4.243 3.422,4.243h48.5c1.657,0 3,-1.343 3,-3s-1.343,-3 -3,-3h-0.5c-1.381,0 -2.5,-1.119 -2.5,-2.5s1.119,-2.5 2.5,-2.5h4.5c1.657,0 3,-1.343 3,-3s-1.343,-3 -3,-3h-8.377c2.141,-3.494 3.377,-7.602 3.377,-12 0,-2.441 -0.384,-4.792 -1.088,-7h6.501c1.139,0 2.202,-0.721 2.497,-1.821 0.445,-1.663 -0.814,-3.179 -2.41,-3.179h-1.5c-1.105,0 -2,-0.895 -2,-2s0.895,-2 2,-2z">
<aapt:attr name="android:fillColor">
<gradient android:centerX="32" android:centerY="31.500021"
android:gradientRadius="32" android:tileMode="mirror" android:type="radial">
<item android:color="#1F0FCCFF" android:offset="0"/>
<item android:color="#1F0FCEFF" android:offset="0.193"/>
<item android:color="#1F0FD5FF" android:offset="0.703"/>
<item android:color="#1F0ECEFF" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M51.483,15.326l3.937,-3.93c1.24,-1.25 0.36,-3.38 -1.4,-3.38H42c-1.1,0 -2,0.9 -2,2v12c0,1.76 2.13,2.65 3.38,1.4l4.537,-4.529C50.532,22.313 52,26.533 52,31c0,11.215 -8.565,20 -19.5,20c-1.381,0 -2.5,1.119 -2.5,2.5s1.119,2.5 2.5,2.5C46.238,56 57,45.019 57,31C57,25.183 55.012,19.699 51.483,15.326z">
<aapt:attr name="android:fillColor">
<gradient android:endX="43.5" android:endY="-21.15"
android:startX="43.5" android:startY="59.034"
android:tileMode="mirror" android:type="linear">
<item android:color="#FFFF634D" android:offset="0"/>
<item android:color="#FFFE6464" android:offset="0.204"/>
<item android:color="#FFFC6581" android:offset="0.521"/>
<item android:color="#FFFA6694" android:offset="0.794"/>
<item android:color="#FFFA669A" android:offset="0.989"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M20.604,38.58l-4.523,4.53C13.468,39.683 12,35.463 12,31c0,-11.215 8.565,-20 19.5,-20c1.381,0 2.5,-1.119 2.5,-2.5S32.881,6 31.5,6C17.762,6 7,16.981 7,31c0,5.815 1.989,11.303 5.52,15.677L8.584,50.62c-1.25,1.25 -0.36,3.38 1.4,3.38h12c1.1,0 2,-0.9 2,-2V39.98C23.984,38.22 21.854,37.34 20.604,38.58z">
<aapt:attr name="android:fillColor">
<gradient android:endX="20.5" android:endY="-24.844"
android:startX="20.5" android:startY="55.227"
android:tileMode="mirror" android:type="linear">
<item android:color="#FFFF634D" android:offset="0"/>
<item android:color="#FFFE6464" android:offset="0.204"/>
<item android:color="#FFFC6581" android:offset="0.521"/>
<item android:color="#FFFA6694" android:offset="0.794"/>
<item android:color="#FFFA669A" android:offset="0.989"/>
</gradient>
</aapt:attr>
</path>
</vector>

View File

@ -0,0 +1,202 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-11.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:pathData="m118,62.129h2.605c3.789,0 7.188,-2.84 7.383,-6.625 0.211,-4.035 -3,-7.375 -6.988,-7.375h-4.805c-1.965,0 -3.785,-1.328 -4.129,-3.262 -0.043,-0.25 -0.066,-0.5 -0.066,-0.746 0.004,-2.168 1.734,-3.93 3.887,-3.988 3.023,-0.082 5.731,-2.293 6.07,-5.297 0.027,-0.242 0.039,-0.477 0.039,-0.711 0,-3.313 -2.688,-5.996 -6,-5.996h-4.602c-0.434,0 -4.863,-0.035 -5.281,-0.105 -0.035,-0.008 -0.074,-0.016 -0.113,-0.023v18h-42v-18h25.715c0.438,-1.688 0.539,-3.512 -0.945,-5.719 -1.836,-2.742 -5.023,-4.281 -8.324,-4.281h-9.445c-1.656,0 -3,-1.344 -3,-3s1.344,-3 3,-3h0.66c3.25,0 6.16,-2.434 6.332,-5.68 0.18,-3.461 -2.57,-6.32 -5.992,-6.32h-57.66c-3.25,0 -6.16,2.434 -6.332,5.68 -0.18,3.457 2.57,6.32 5.992,6.32h15c1.656,0 3,1.344 3,3s-1.344,3 -3,3h-17c-4.418,0 -8,3.582 -8,8s3.582,8 8,8h26v12h-14l2,21.109c-1.07,0.801 -1.836,1.98 -1.973,3.375 -0.184,1.84 0.633,3.5 1.973,4.504v5.047c-0.027,0.148 -0.043,0.297 -0.027,0.449 0.301,2.992 -2.039,5.516 -4.973,5.516h-14.66c-2.984,0 -5.762,2.023 -6.25,4.965 -0.246,1.465 0.043,2.848 0.684,4.012 1.074,1.938 3.234,3.023 5.449,3.023h3.164c2.375,0 4.207,1.328 4.551,3.27 0.039,0.246 0.063,0.488 0.063,0.723 0.004,2.215 -1.789,4.008 -3.996,4.008h-0.004c-2.27,0 -4.473,1.203 -5.398,3.277 -1.98,4.426 1.207,8.723 5.398,8.723h62c3.313,0 6,-2.688 6,-6s-2.688,-6 -6,-6h-10v-20l46,0.129h9.66c3.141,0 6.168,-2.539 6.328,-5.676 0.184,-3.461 -2.57,-6.324 -5.992,-6.324h-0.023,-0.035c-1.555,0 -3.078,-0.508 -4.156,-1.535 -0.34,-0.324 -0.637,-0.699 -0.875,-1.129 -2.574,-4.637 0.711,-9.336 5.094,-9.336z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="80.322"
android:centerX="60.334"
android:centerY="65.146"
android:type="radial"
android:tileMode="mirror">
<item android:offset="0" android:color="#1F0FCCFF"/>
<item android:offset="0.1927" android:color="#1F0FCEFF"/>
<item android:offset="0.7025" android:color="#1F0FD5FF"/>
<item android:offset="1" android:color="#1F0FD7FF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m18,100v-60h92v60c0,5.523 -4.477,10 -10,10h-72c-5.523,0 -10,-4.477 -10,-10z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="110"
android:startX="64"
android:endY="40"
android:endX="64"
android:type="linear"
android:tileMode="mirror">
<item android:offset="0" android:color="#FFFFC662"/>
<item android:offset="0.0036" android:color="#FFFFC662"/>
<item android:offset="0.6085" android:color="#FFFFC582"/>
<item android:offset="1" android:color="#FFFFC491"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m110,29.602v16.398h-92v-16.398c0,-5.309 4.332,-9.602 9.684,-9.602h72.633c5.352,0 9.684,4.293 9.684,9.602">
<aapt:attr name="android:fillColor">
<gradient
android:startY="46"
android:startX="64"
android:endY="20"
android:endX="64"
android:type="linear"
android:tileMode="mirror">
<item android:offset="0" android:color="#FFFF634D"/>
<item android:offset="0.2083" android:color="#FFFD6464"/>
<item android:offset="0.5223" android:color="#FFFC6582"/>
<item android:offset="0.7935" android:color="#FFFA6694"/>
<item android:offset="0.9892" android:color="#FFFA669A"/>
<item android:offset="1" android:color="#FFFA669A"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m49.309,20h-12.621c-2.832,1.988 -4.688,5.277 -4.688,9 0,6.07 4.93,11 11,11 6.07,0 11,-4.93 11,-11 0,-3.723 -1.855,-7.012 -4.691,-9z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="40"
android:startX="43"
android:endY="20"
android:endX="43"
android:type="linear"
android:tileMode="mirror">
<item android:offset="0" android:color="#FFFF5840"/>
<item android:offset="0.0072" android:color="#FFFF5840"/>
<item android:offset="0.9892" android:color="#FFFA528C"/>
<item android:offset="1" android:color="#FFFA528C"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m43,34c-2.75,0 -5,-2.25 -5,-5v-12c0,-2.75 2.25,-5 5,-5s5,2.25 5,5v12c0,2.75 -2.25,5 -5,5z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="12"
android:startX="43"
android:endY="34"
android:endX="43"
android:type="linear"
android:tileMode="mirror">
<item android:offset="0" android:color="#FFA4A4A4"/>
<item android:offset="0.6301" android:color="#FF7F7F7F"/>
<item android:offset="1" android:color="#FF6F6F6F"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m91.309,20h-12.621c-2.832,1.988 -4.688,5.277 -4.688,9 0,6.07 4.93,11 11,11 6.07,0 11,-4.93 11,-11 0,-3.723 -1.855,-7.012 -4.691,-9z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="40"
android:startX="85"
android:endY="20"
android:endX="85"
android:type="linear"
android:tileMode="mirror">
<item android:offset="0" android:color="#FFFF5840"/>
<item android:offset="0.0072" android:color="#FFFF5840"/>
<item android:offset="0.9892" android:color="#FFFA528C"/>
<item android:offset="1" android:color="#FFFA528C"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m85,34c-2.75,0 -5,-2.25 -5,-5v-12c0,-2.75 2.25,-5 5,-5s5,2.25 5,5v12c0,2.75 -2.25,5 -5,5z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="12"
android:startX="85"
android:endY="34"
android:endX="85"
android:type="linear"
android:tileMode="mirror">
<item android:offset="0" android:color="#FFA4A4A4"/>
<item android:offset="0.6301" android:color="#FF7F7F7F"/>
<item android:offset="1" android:color="#FF6F6F6F"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m78,81.008c0,2.762 2.238,5 5,4.996h2.992c3.316,-0.008 6.008,2.684 6.008,5.996s-2.688,6 -6,6h-8c-3.313,0 -6,2.688 -6,6s2.688,6 6,6h22c5.523,0 10,-4.477 10,-10v-24l-27.008,0.012c-2.758,-0 -4.992,2.234 -4.992,4.996z"
android:fillColor="#ffb86a"/>
<path
android:pathData="m18,68h17.785c1.992,0 3.84,-1.363 4.16,-3.328 0.406,-2.508 -1.516,-4.672 -3.945,-4.672h-3c-1.656,0 -3,-1.344 -3,-3s1.344,-3 3,-3h14.785c1.992,0 3.84,-1.363 4.16,-3.328 0.406,-2.508 -1.516,-4.672 -3.945,-4.672h-30z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="108.25"
android:startX="35"
android:endY="43.742"
android:endX="35"
android:type="linear"
android:tileMode="mirror">
<item android:offset="0" android:color="#FFFFCE76"/>
<item android:offset="0.0036" android:color="#FFFFCE76"/>
<item android:offset="0.6054" android:color="#FFFFCD92"/>
<item android:offset="1" android:color="#FFFFCCA0"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m98,62c0,3.313 -2.688,6 -6,6s-6,-2.688 -6,-6 2.688,-6 6,-6 6,2.688 6,6z"
android:fillColor="#ffb977"/>
<path
android:pathData="m48,74h-8c-2.211,0 -4,-1.789 -4,-4v-8c0,-2.211 1.789,-4 4,-4h8c2.211,0 4,1.789 4,4v8c0,2.211 -1.789,4 -4,4zM72,70v-8c0,-2.211 -1.789,-4 -4,-4h-8c-2.211,0 -4,1.789 -4,4v8c0,2.211 1.789,4 4,4h8c2.211,0 4,-1.789 4,-4zM92,70v-8c0,-2.211 -1.789,-4 -4,-4h-8c-2.211,0 -4,1.789 -4,4v8c0,2.211 1.789,4 4,4h8c2.211,0 4,-1.789 4,-4zM52,90v-8c0,-2.211 -1.789,-4 -4,-4h-8c-2.211,0 -4,1.789 -4,4v8c0,2.211 1.789,4 4,4h8c2.211,0 4,-1.789 4,-4zM72,90v-8c0,-2.211 -1.789,-4 -4,-4h-8c-2.211,0 -4,1.789 -4,4v8c0,2.211 1.789,4 4,4h8c2.211,0 4,-1.789 4,-4zM92,90v-8c0,-2.211 -1.789,-4 -4,-4h-8c-2.211,0 -4,1.789 -4,4v8c0,2.211 1.789,4 4,4h8c2.211,0 4,-1.789 4,-4z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="94"
android:startX="64"
android:endY="58"
android:endX="64"
android:type="linear"
android:tileMode="mirror">
<item android:offset="0" android:color="#FFFFE79F"/>
<item android:offset="0.1186" android:color="#FFFFE9A6"/>
<item android:offset="1" android:color="#FFFFF5D5"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m124,104c0,11.047 -8.953,20 -20,20 -11.047,0 -20,-8.953 -20,-20 0,-11.047 8.953,-20 20,-20 11.047,0 20,8.953 20,20z"
android:fillColor="#fff"/>
<path
android:pathData="m104,80c-13.254,0 -24,10.746 -24,24 0,13.254 10.746,24 24,24 13.254,0 24,-10.746 24,-24 0,-13.254 -10.746,-24 -24,-24zM104,120c-8.836,0 -16,-7.164 -16,-16 0,-8.836 7.164,-16 16,-16 8.836,0 16,7.164 16,16 0,8.836 -7.164,16 -16,16z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="128"
android:startX="104"
android:endY="80"
android:endX="104"
android:type="linear"
android:tileMode="mirror">
<item android:offset="0" android:color="#FF155CDE"/>
<item android:offset="0.6248" android:color="#FF2289E7"/>
<item android:offset="1" android:color="#FF289FEC"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m113.41,110.59 l-5.563,-5.563c0.086,-0.328 0.148,-0.668 0.148,-1.023 0,-1.477 -0.809,-2.754 -2,-3.445v-6.555c0,-1.105 -0.895,-2 -2,-2s-2,0.895 -2,2v6.555c-1.191,0.691 -2,1.969 -2,3.445 0,2.211 1.789,4 4,4 0.355,0 0.695,-0.063 1.023,-0.148l5.563,5.563c0.391,0.391 0.902,0.586 1.414,0.586s1.023,-0.195 1.414,-0.586c0.781,-0.781 0.781,-2.047 0,-2.828z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="92"
android:startX="107"
android:endY="114"
android:endX="107"
android:type="linear"
android:tileMode="mirror">
<item android:offset="0" android:color="#FF919191"/>
<item android:offset="1" android:color="#FF6F6F6F"/>
</gradient>
</aapt:attr>
</path>
</vector>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-10.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:shape="rectangle">
<corners android:topLeftRadius="4dp" android:topRightRadius="4dp" />
<solid android:color="#2196f3" tools:color="?timetable_lesson_change_color" />
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<solid android:color="@color/colorSurface_4dp" />
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<stroke android:width="1dp" android:color="#1e000000" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-10.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:topLeftRadius="4dp" android:topRightRadius="4dp" />
<solid android:color="#e91e63" />
</shape>

View File

@ -36,24 +36,54 @@
android:layout_height="wrap_content"
android:visibility="gone"
android:orientation="vertical"
tools:visibility="visible">
tools:visibility="visible"
tools:ignore="HardcodedText">
<com.google.android.material.button.MaterialButton
android:id="@+id/getLogs"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="Save Debug Logs" />
<com.google.android.material.button.MaterialButton
android:id="@+id/librusCaptchaButton"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="Librus Captcha" />
<com.google.android.material.button.MaterialButton
android:id="@+id/runChucker"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="Launch Chucker" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:gravity="center">
<com.google.android.material.button.MaterialButton
android:id="@+id/mobidziennikMessagesSwitch"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_margin="8dp"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_weight="1"
android:text="Zmień moduł wiadomości" />
<com.google.android.material.button.MaterialButton
android:id="@+id/composeButton"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_weight="1"
android:text="Compose" />
</LinearLayout>

View File

@ -151,6 +151,16 @@
</FrameLayout>
</LinearLayout>
<Switch
android:id="@+id/fakeLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="24dp"
android:layout_marginTop="16dp"
android:layout_marginRight="24dp"
android:text="Fake login" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/timetableLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="gone">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorSurface"
style="@style/Widget.MaterialComponents.AppBarLayout.Surface">
<com.nshmura.recyclertablayout.RecyclerTabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/colorSurface_6dp"
app:rtl_tabTextAppearance="@style/rtl_RecyclerTabLayout.Tab"
app:rtl_tabIndicatorColor="?colorPrimary"
app:rtl_tabMinWidth="90dp"
app:rtl_tabMaxWidth="300dp"
app:rtl_tabSelectedTextColor="?colorPrimary"
app:rtl_tabPaddingStart="16dp"
app:rtl_tabPaddingEnd="16dp"
app:rtl_tabPaddingTop="12dp"
app:rtl_tabPaddingBottom="12dp"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<LinearLayout
android:id="@+id/timetableNotPublicLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
android:gravity="center"
tools:visibility="visible">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_no_timetable" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/timetable_not_public_title"
android:textSize="24sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/timetable_not_public_text"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/timetable_not_public_hint"
android:textSize="14sp" />
</LinearLayout>
</FrameLayout>
</layout>

View File

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ScrollView
android:id="@+id/dayScroll"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.linkedin.android.tachyon.DayView
android:id="@+id/day"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
app:dividerHeight="1dp"
app:endHour="18"
app:eventMargin="2dp"
app:halfHourDividerColor="#e0e0e0"
app:halfHourHeight="60dp"
app:hourDividerColor="#b0b0b0"
app:hourLabelMarginEnd="10dp"
app:hourLabelWidth="40dp"
app:startHour="5"
tools:visibility="gone"/>
</ScrollView>
<LinearLayout
android:id="@+id/noLessonsLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
android:gravity="center"
tools:visibility="gone">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_timetable" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/timetable_no_lessons_title"
android:textSize="24sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/freeDayLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
android:gravity="center"
tools:visibility="gone">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_sunbed" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/timetable_free_day_title"
android:textSize="24sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/timetable_free_day_text"
android:textSize="14sp" />
<TextView
android:id="@+id/freeDayText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:gravity="center"
tools:text="Dzień wolny dla szkoły z puli dyrektorskiej z okazji obchodów Światowego Dnia Wtorku w mieście Poznań i na przedmieśiach"
android:textSize="16sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/freeDayShowTimetable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/timetable_free_day_show" />
</LinearLayout>
<LinearLayout
android:id="@+id/noTimetableLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
android:gravity="center"
tools:visibility="visible">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_sync" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/timetable_no_timetable_title"
android:textSize="24sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/timetable_no_timetable_text"
android:textSize="16sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/noTimetableSync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/timetable_no_timetable_sync" />
<TextView
android:id="@+id/noTimetableWeek"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/timetable_no_timetable_week"
android:textSize="12sp"
android:textStyle="italic"/>
</LinearLayout>
</FrameLayout>
</layout>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2019 LinkedIn Corporation -->
<!-- All Rights Reserved. -->
<!-- -->
<!-- Licensed under the BSD 2-Clause License (the "License"). See License in the project root -->
<!-- for license information. -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Base.TextAppearance.AppCompat.Small"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:ellipsize="end"
android:maxLines="1"
android:textAlignment="viewEnd"
tools:text="1 PM" />

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="annotationVisible"
type="boolean"/>
<variable
name="lessonNumber"
type="Integer" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:colorBackground"
android:foreground="@drawable/bg_rounded_ripple_4dp"
tools:padding="32dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout_height="90dp"
android:background="?timetable_lesson_bg"
android:orientation="vertical"
android:paddingBottom="4dp">
<TextView
android:id="@+id/annotation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/timetable_lesson_annotation"
android:fontFamily="sans-serif-condensed"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/timetable_lesson_cancelled"
android:textColor="#000"
android:textSize="12sp"
android:textStyle="italic"
android:visibility="@{annotationVisible ? View.VISIBLE : View.GONE}"
tools:text="Zastępstwo: zamiast lekcji język polski z Adam Dodatkowy"
tools:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:orientation="horizontal"
android:baselineAligned="false">
<!--tools:background="@drawable/timetable_subject_color_rounded"-->
<TextView
android:id="@+id/subjectName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_weight="1"
android:ellipsize="end"
android:fontFamily="sans-serif-light"
android:maxLines="@{annotationVisible ? 1 : 2}"
android:textSize="16sp"
android:textStyle="bold"
app:autoSizeMaxTextSize="16sp"
app:autoSizeMinTextSize="12sp"
app:autoSizeTextType="uniform"
tools:maxLines="2"
tools:text="pracownia urządzeń techniki komputerowej" />
<ImageView
android:id="@+id/attendanceIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:visibility="gone"
tools:srcCompat="@sample/check"
tools:visibility="visible" />
<ImageView
android:id="@+id/imageView4"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_weight="0"
app:srcCompat="@drawable/bg_circle"
android:visibility="gone" />
<TextView
android:id="@+id/textView6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:fontFamily="sans-serif-condensed-light"
android:includeFontPadding="false"
android:layout_marginBottom="-4dp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:text="@{Integer.toString(lessonNumber)}"
android:textSize="28sp"
android:visibility="@{lessonNumber != null ? View.VISIBLE : View.GONE}"
tools:text="3"/>
<!--android:layout_marginTop="@{annotationVisible ? `-4dp` : `4dp`}"
android:layout_marginBottom="@{annotationVisible ? `-4dp` : `0dp`}"-->
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="bottom"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<TextView
android:id="@+id/detailsFirst"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
tools:text="8:10 - 8:55 • 015 językowa → 016 językowa" />
<TextView
android:id="@+id/detailsSecond"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:singleLine="true"
android:textAppearance="@style/NavView.TextView.Helper"
android:textSize="12sp"
tools:text="Paweł Informatyczny • 2b3T n1" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
</layout>

View File

@ -3,4 +3,9 @@
<attr name="colorSection" format="color" />
<attr name="cardBackgroundDimmed" format="color" />
<attr name="cardBackgroundHighlight" format="color" />
<attr name="timetable_lesson_bg" format="reference" />
<attr name="timetable_lesson_cancelled_color" format="color" />
<attr name="timetable_lesson_change_color" format="color" />
<attr name="timetable_lesson_shifted_source_color" format="color" />
<attr name="timetable_lesson_shifted_target_color" format="color" />
</resources>

View File

@ -930,6 +930,7 @@
<string name="settings_register_show_teacher_absences_text">Pokazuj nieobecności nauczycieli w Terminarzu</string>
<string name="edziennik_progress_endpoint_school_info">Pobieram informacje o szkole...</string>
<string name="edziennik_progress_endpoint_grades">Pobieranie ocen ucznia...</string>
<string name="edziennik_progress_endpoint_grade_categories">Pobieranie kategorii ocen...</string>
<string name="edziennik_progress_login_template_web">Logowanie do Template WEB...</string>
<string name="edziennik_progress_login_template_api">Logowanie do Template API...</string>
<string name="edziennik_progress_login_mobidziennik_web">Logowanie do MobiDziennika...</string>
@ -983,4 +984,26 @@
<string name="dont_ask_again">Nie pytaj ponownie</string>
<string name="app_manager_open_failed">Nie udało się otworzyć ustawień</string>
<string name="edziennik_notification_api_notify_title">Tworzenie powiadomień</string>
<string name="login_librus_captcha_title">Librus - logowanie</string>
<string name="timetable_today">Dzisiaj</string>
<string name="timetable_lesson_cancelled">Lekcja odwołana</string>
<string name="timetable_lesson_change">Zastępstwo</string>
<string name="timetable_lesson_change_format">Zastępstwo: zamiast %s</string>
<string name="timetable_lesson_shifted_same_day">Lekcja przeniesiona na godz. %s</string>
<string name="timetable_lesson_shifted_other_day">Lekcja przeniesiona na %s, godz. %s</string>
<string name="timetable_lesson_shifted_from_same_day">Lekcja przeniesiona z godz. %s</string>
<string name="timetable_lesson_shifted_from_other_day">Lekcja przeniesiona z dnia %s, godz. %s</string>
<string name="timetable_lesson_shifted">Lekcja przeniesiona na inny termin</string>
<string name="timetable_lesson_shifted_from">Lekcja przeniesiona z innego terminu</string>
<string name="timetable_not_public_title">Brak planu lekcji</string>
<string name="timetable_not_public_text">Plan lekcji nie został opublikowany przez szkołę.</string>
<string name="timetable_not_public_hint">Skontaktuj się z wychowawcą w celu udostępnienia planu lekcji.</string>
<string name="timetable_free_day_title">Dzień wolny</string>
<string name="timetable_free_day_text">W tym dniu nie ma lekcji:</string>
<string name="timetable_free_day_show">Pokaż plan lekcji</string>
<string name="timetable_no_lessons_title">Brak lekcji tego dnia</string>
<string name="timetable_no_timetable_title">Brak planu lekcji</string>
<string name="timetable_no_timetable_text">Nie pobrano planu lekcji na ten tydzień.</string>
<string name="timetable_no_timetable_sync">Pobierz plan lekcji</string>
<string name="timetable_no_timetable_week">na tydzień %s</string>
</resources>

View File

@ -1,4 +1,4 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<resources>
<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
<item name="colorAccent">@color/colorAccent</item>
@ -95,6 +95,12 @@
<item name="mal_color_secondary">?android:textColorSecondary</item>
<item name="mal_card_background">?colorSurface</item>
<item name="mal_divider_color">@color/dividerColor</item>
<item name="timetable_lesson_bg">@drawable/timetable_lesson_bg_light</item>
<item name="timetable_lesson_cancelled_color">#9f9f9f</item>
<item name="timetable_lesson_change_color">#ffb300</item>
<item name="timetable_lesson_shifted_source_color">#A1887F</item>
<item name="timetable_lesson_shifted_target_color">#4caf50</item>
</style>
<style name="AppTheme.Dark" parent="NavView.Dark">
<item name="colorPrimary">#64b5f6</item>
@ -119,6 +125,12 @@
<item name="mal_color_secondary">@color/secondaryTextDark</item>
<item name="mal_card_background">?colorSurface</item>
<item name="mal_divider_color">@color/dividerColor</item>
<item name="timetable_lesson_bg">@drawable/timetable_lesson_bg_dark</item>
<item name="timetable_lesson_cancelled_color">#838383</item>
<item name="timetable_lesson_change_color">#ffb300</item>
<item name="timetable_lesson_shifted_source_color">#A1887F</item>
<item name="timetable_lesson_shifted_target_color">#4caf50</item>
</style>

View File

@ -5,8 +5,8 @@ buildscript {
kotlin_version = '1.3.50'
release = [
versionName: "3.1.1",
versionCode: 3010199
versionName: "3.9.4-dev",
versionCode: 3090400
]
setup = [