[Messages] Add sending messages and fix stuff.

This commit is contained in:
Kuba Szczodrzyński 2020-01-02 18:27:58 +01:00
parent f292b3637d
commit eae7189981
91 changed files with 2897 additions and 2095 deletions

45
.idea/jarRepositories.xml Normal file
View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://kotlin.bintray.com/kotlinx/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="https://dl.bintray.com/wulkanowy/wulkanowy" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
</component>
</project>

View File

@ -0,0 +1,13 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-12-28.
-->
<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="#FF000000"
android:pathData="M13.5,15.5H10V12.5H13.5A1.5,1.5 0,0 1,15 14A1.5,1.5 0,0 1,13.5 15.5M10,6.5H13A1.5,1.5 0,0 1,14.5 8A1.5,1.5 0,0 1,13 9.5H10M15.6,10.79C16.57,10.11 17.25,9 17.25,8C17.25,5.74 15.5,4 13.25,4H7V18H14.04C16.14,18 17.75,16.3 17.75,14.21C17.75,12.69 16.89,11.39 15.6,10.79Z"/>
</vector>

View File

@ -0,0 +1,13 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-12-28.
-->
<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="#FF000000"
android:pathData="M10,4V7H12.21L8.79,15H6V18H14V15H11.79L15.21,7H18V4H10Z"/>
</vector>

View File

@ -0,0 +1,13 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-12-28.
-->
<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="#FF000000"
android:pathData="M5,21H19V19H5V21M12,17A6,6 0,0 0,18 11V3H15.5V11A3.5,3.5 0,0 1,12 14.5A3.5,3.5 0,0 1,8.5 11V3H6V11A6,6 0,0 0,12 17Z"/>
</vector>

View File

@ -32,11 +32,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.modules.messages.MessagesComposeActivity"
android:configChanges="orientation|screenSize"
android:label="@string/messages_compose_title"
android:theme="@style/AppTheme.Black" />
<activity
android:name=".ui.modules.feedback.FeedbackActivity"
android:configChanges="orientation|screenSize|keyboardHidden"

View File

@ -1,6 +1,7 @@
<h3>Wersja 4.0-beta.1, 2020-01-02</h3>
<ul>
<li><b>Przebudowaliśmy cały moduł synchronizacji</b>, co oznacza większą stabilność aplikacji, szybkosć oraz poprawność pobieranych danych.</li>
<li><b><u>Wysyłanie wiadomości</u></b> - funkcja, na którą czekał każdy. Od teraz w Szkolnym można wysyłać oraz odpowiadać na wiadomości do nauczycieli &#x1F44F;</li>
<li>Udoskonalony wygląd Szkolnego - sprawi, że korzystanie z aplikacji będzie jeszcze przyjemniejsze</li>
<li>Nowa <b>Strona główna</b> - ładniejszy wygląd oraz możliwość przestawiania kart na każdym profilu</li>
<li>Nowy <b>Plan lekcji</b> - z doskonałą obsługą lekcji przesuniętych oraz dwóch lekcji o tej samej godzinie</li>
@ -20,10 +21,9 @@
<b>Uwaga.</b> Ponieważ to wersja <i>beta</i>, niektóre funkcje mogą nie działać prawidłowo.<br>
Staramy się usuwać takie przypadki, jednak na chwilę obecną mogą występować błędy w:
<ul>
<li>Wysyłanie wiadomości może czasami nie działać - proszę o zgłaszanie wszystkich błędów na naszym serwerze Discord</li>
<li>Widget powiadomień</li>
<li>Zgłaszanie błędów</li>
<li>Terminarz - brak informacji o odwołanych lekcjach w dialogu</li>
<li>Obsługa błędów - rzadko występuje</li>
<li>Brak generowania blokowego planu lekcji</li>
</ul>
<br>

View File

@ -67,8 +67,9 @@ import me.leolin.shortcutbadger.ShortcutBadger;
import okhttp3.ConnectionSpec;
import okhttp3.OkHttpClient;
import okhttp3.TlsVersion;
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing;
import pl.szczodrzynski.edziennik.config.Config;
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing;
import pl.szczodrzynski.edziennik.data.api.task.EdziennikTask;
import pl.szczodrzynski.edziennik.data.db.AppDb;
import pl.szczodrzynski.edziennik.data.db.modules.debuglog.DebugLog;
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile;
@ -438,6 +439,11 @@ public class App extends androidx.multidex.MultiDexApplication implements Config
"Vulcan"
);
if (config.getRunSync()) {
config.setRunSync(false);
EdziennikTask.Companion.sync().enqueue(this);
}
try {
final long startTime = System.currentTimeMillis();
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(instanceIdResult -> {

View File

@ -4,6 +4,7 @@ import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
@ -15,6 +16,7 @@ import android.text.*
import android.text.style.ForegroundColorSpan
import android.text.style.StrikethroughSpan
import android.text.style.StyleSpan
import android.util.Base64
import android.util.Base64.NO_WRAP
import android.util.Base64.encodeToString
import android.util.LongSparseArray
@ -42,10 +44,10 @@ import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.data.db.modules.teams.Team
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.navlib.getColorFromRes
import java.io.PrintWriter
import java.io.StringWriter
import java.math.BigInteger
import java.nio.charset.Charset
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.*
@ -99,12 +101,31 @@ fun Bundle?.getString(key: String, defaultValue: String): String {
return this?.getString(key, defaultValue) ?: defaultValue
}
fun String.fixName(): String {
return this.fixWhiteSpaces().toProperCase()
/**
* ` The quick BROWN_fox Jumps OveR THE LAZy-DOG. `
*
* converts to
*
* `The Quick Brown_fox Jumps Over The Lazy-Dog.`
*/
fun String?.fixName(): String {
return this?.fixWhiteSpaces()?.toProperCase() ?: ""
}
/**
* `The quick BROWN_fox Jumps OveR THE LAZy-DOG.`
*
* converts to
*
* `The Quick Brown_fox Jumps Over The Lazy-Dog.`
*/
fun String.toProperCase(): String = changeStringCase(this)
/**
* `John Smith` -> `Smith John`
*
* `JOHN SMith` -> `SMith JOHN`
*/
fun String.swapFirstLastName(): String {
return this.split(" ").let {
if (it.size > 1)
@ -150,33 +171,52 @@ fun String.getShortName(): String {
}
}
fun List<String>.join(delimiter: String): String {
return this.joinToString(delimiter)
/**
* "John Smith" -> "JS"
*
* "JOHN SMith" -> "JS"
*
* "John" -> "J"
*
* "John " -> "J"
*
* "John Smith " -> "JS"
*
* " " -> ""
*
* " " -> ""
*/
fun String?.getNameInitials(): String {
if (this.isNullOrBlank()) return ""
return this.toUpperCase().fixWhiteSpaces().split(" ").take(2).map { it[0] }.joinToString("")
}
fun colorFromName(context: Context, name: String?): Int {
fun List<String>.join(delimiter: String): String {
return concat(delimiter).toString()
}
fun colorFromName(name: String?): Int {
var crc = (name ?: "").crc16()
crc = (crc and 0xff) or (crc shr 8)
crc %= 16
val color = when (crc) {
13 -> R.color.md_red_500
4 -> R.color.md_pink_A400
2 -> R.color.md_purple_A400
9 -> R.color.md_deep_purple_A700
5 -> R.color.md_indigo_500
1 -> R.color.md_indigo_A700
6 -> R.color.md_cyan_A200
14 -> R.color.md_teal_400
15 -> R.color.md_green_500
7 -> R.color.md_yellow_A700
3 -> R.color.md_deep_orange_A400
8 -> R.color.md_deep_orange_A700
10 -> R.color.md_brown_500
12 -> R.color.md_grey_400
11 -> R.color.md_blue_grey_400
else -> R.color.md_light_green_A700
}
return context.getColorFromRes(color)
return when (crc) {
13 -> 0xffF44336
4 -> 0xffF50057
2 -> 0xffD500F9
9 -> 0xff6200EA
5 -> 0xff3F51B5
1 -> 0xff304FFE
6 -> 0xff18FFFF
14 -> 0xff26A69A
15 -> 0xff4CAF50
7 -> 0xffFFD600
3 -> 0xffFF3D00
8 -> 0xffDD2C00
10 -> 0xff795548
12 -> 0xffBDBDBD
11 -> 0xff78909C
else -> 0xff64DD17
}.toInt()
}
fun colorFromCssName(name: String): Int {
@ -242,6 +282,21 @@ fun <T> SparseArray<T>.values(): List<T> {
return result
}
fun <K, V> List<Pair<K, V>>.keys(): List<K> {
val result = mutableListOf<K>()
forEach { pair ->
result += pair.first
}
return result
}
fun <K, V> List<Pair<K, V>>.values(): List<V> {
val result = mutableListOf<V>()
forEach { pair ->
result += pair.second
}
return result
}
fun <T> List<T>.toSparseArray(destination: SparseArray<T>, key: (T) -> Int) {
forEach {
destination.put(key(it), it)
@ -429,6 +484,24 @@ fun CharSequence?.asBoldSpannable(): Spannable {
spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return spannable
}
fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignoreCase: Boolean = false): Spannable {
val spannable = SpannableString(this)
if (substring == null) {
spans.forEach {
spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
else if (substring.isNotEmpty()) {
var index = indexOf(substring, ignoreCase = ignoreCase)
while (index >= 0) {
spans.forEach {
spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
index = indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase);
}
}
return spannable
}
/**
* Returns a new read-only list only of those given elements, that are not empty.
@ -436,7 +509,7 @@ fun CharSequence?.asBoldSpannable(): Spannable {
*/
fun <T : CharSequence> listOfNotEmpty(vararg elements: T): List<T> = elements.filterNot { it.isEmpty() }
fun List<CharSequence?>.concat(delimiter: String? = null): CharSequence {
fun List<CharSequence?>.concat(delimiter: CharSequence? = null): CharSequence {
if (this.isEmpty()) {
return ""
}
@ -445,11 +518,13 @@ fun List<CharSequence?>.concat(delimiter: String? = null): CharSequence {
return this[0] ?: ""
}
var spanned = false
for (piece in this) {
if (piece is Spanned) {
spanned = true
break
var spanned = delimiter is Spanned
if (!spanned) {
for (piece in this) {
if (piece is Spanned) {
spanned = true
break
}
}
}
@ -497,8 +572,44 @@ fun JsonObject(vararg properties: Pair<String, Any?>): JsonObject {
}
}
fun JsonArray(vararg properties: Any?): JsonArray {
return JsonArray().apply {
for (property in properties) {
when (property) {
is JsonElement -> add(property as JsonElement?)
is String -> add(property as String?)
is Char -> add(property as Char?)
is Number -> add(property as Number?)
is Boolean -> add(property as Boolean?)
}
}
}
}
fun Bundle(vararg properties: Pair<String, Any?>): Bundle {
return Bundle().apply {
for (property in properties) {
when (property.second) {
is String -> putString(property.first, property.second as String?)
is Char -> putChar(property.first, property.second as Char)
is Int -> putInt(property.first, property.second as Int)
is Long -> putLong(property.first, property.second as Long)
is Float -> putFloat(property.first, property.second as Float)
is Short -> putShort(property.first, property.second as Short)
is Double -> putDouble(property.first, property.second as Double)
is Boolean -> putBoolean(property.first, property.second as Boolean)
}
}
}
}
fun JsonArray?.isNullOrEmpty(): Boolean = (this?.size() ?: 0) == 0
fun JsonArray.isEmpty(): Boolean = this.size() == 0
operator fun JsonArray.plusAssign(o: JsonElement) = this.add(o)
operator fun JsonArray.plusAssign(o: String) = this.add(o)
operator fun JsonArray.plusAssign(o: Char) = this.add(o)
operator fun JsonArray.plusAssign(o: Number) = this.add(o)
operator fun JsonArray.plusAssign(o: Boolean) = this.add(o)
@Suppress("UNCHECKED_CAST")
inline fun <T : View> T.onClick(crossinline onClickListener: (v: T) -> Unit) {
@ -696,3 +807,75 @@ val Throwable.stackTraceString: String
printStackTrace(PrintWriter(sw))
return sw.toString()
}
inline fun <T> LongSparseArray<T>.filter(predicate: (T) -> Boolean): List<T> {
val destination = ArrayList<T>()
this.forEach { _, element -> if (predicate(element)) destination.add(element) }
return destination
}
fun CharSequence.replace(oldValue: String, newValue: CharSequence, ignoreCase: Boolean = false): CharSequence =
splitToSequence(oldValue, ignoreCase = ignoreCase).toList().concat(newValue)
fun Int.toColorStateList(): ColorStateList {
val states = arrayOf(
intArrayOf( android.R.attr.state_enabled ),
intArrayOf(-android.R.attr.state_enabled ),
intArrayOf(-android.R.attr.state_checked ),
intArrayOf( android.R.attr.state_pressed )
)
val colors = intArrayOf(
this,
this,
this,
this
)
return ColorStateList(states, colors);
}
fun SpannableStringBuilder.appendText(text: CharSequence): SpannableStringBuilder {
append(text)
return this
}
fun SpannableStringBuilder.appendSpan(text: CharSequence, what: Any, flags: Int): SpannableStringBuilder {
val start: Int = length
append(text)
setSpan(what, start, length, flags)
return this
}
fun joinNotNullStrings(delimiter: String = "", vararg parts: String?): String {
var first = true
val sb = StringBuilder()
for (part in parts) {
if (part == null)
continue
if (!first)
sb += delimiter
first = false
sb += part
}
return sb.toString()
}
fun String.notEmptyOrNull(): String? {
return if (isEmpty())
null
else
this
}
fun String.base64Encode(): String {
return encodeToString(toByteArray(), NO_WRAP)
}
fun ByteArray.base64Encode(): String {
return encodeToString(this, NO_WRAP)
}
fun String.base64Decode(): ByteArray {
return Base64.decode(this, Base64.DEFAULT)
}
fun String.base64DecodeToString(): String {
return Base64.decode(this, Base64.DEFAULT).toString(Charset.defaultCharset())
}

View File

@ -23,6 +23,7 @@ import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.ColorUtils
import androidx.lifecycle.Observer
import androidx.navigation.NavOptions
import androidx.recyclerview.widget.RecyclerView
import com.danimahardhika.cafebar.CafeBar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.mikepenz.iconics.IconicsColor
@ -53,6 +54,7 @@ import pl.szczodrzynski.edziennik.ui.modules.agenda.AgendaFragment
import pl.szczodrzynski.edziennik.ui.modules.announcements.AnnouncementsFragment
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceFragment
import pl.szczodrzynski.edziennik.ui.modules.base.DebugFragment
import pl.szczodrzynski.edziennik.ui.modules.base.MainSnackbar
import pl.szczodrzynski.edziennik.ui.modules.behaviour.BehaviourFragment
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar
import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackFragment
@ -63,7 +65,9 @@ import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment
import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkFragment
import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity
import pl.szczodrzynski.edziennik.ui.modules.messages.MessageFragment
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesComposeFragment
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesListFragment
import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsNewFragment
@ -120,6 +124,7 @@ class MainActivity : AppCompatActivity() {
const val TARGET_HELP = 502
const val TARGET_FEEDBACK = 120
const val TARGET_MESSAGES_DETAILS = 503
const val TARGET_MESSAGES_COMPOSE = 504
const val TARGET_WEB_PUSH = 140
const val HOME_ID = DRAWER_ITEM_HOME
@ -211,7 +216,8 @@ class MainActivity : AppCompatActivity() {
list += NavTarget(TARGET_GRADES_EDITOR, R.string.menu_grades_editor, GradesEditorFragment::class)
list += NavTarget(TARGET_HELP, R.string.menu_help, HelpFragment::class)
list += NavTarget(TARGET_FEEDBACK, R.string.menu_feedback, FeedbackFragment::class)
list += NavTarget(TARGET_MESSAGES_DETAILS, R.string.menu_message, MessageFragment::class)
list += NavTarget(TARGET_MESSAGES_DETAILS, R.string.menu_message, MessageFragment::class).withPopTo(DRAWER_ITEM_MESSAGES)
list += NavTarget(TARGET_MESSAGES_COMPOSE, R.string.menu_message_compose, MessagesComposeFragment::class)
list += NavTarget(TARGET_WEB_PUSH, R.string.menu_web_push, WebPushFragment::class)
list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class)
@ -223,6 +229,7 @@ class MainActivity : AppCompatActivity() {
val navView: NavView by lazy { b.navView }
val drawer: NavDrawer by lazy { navView.drawer }
val bottomSheet: NavBottomSheet by lazy { navView.bottomSheet }
val mainSnackbar: MainSnackbar by lazy { MainSnackbar(this) }
val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) }
val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout }
@ -233,10 +240,11 @@ class MainActivity : AppCompatActivity() {
private val fragmentManager by lazy { supportFragmentManager }
private lateinit var navTarget: NavTarget
private var navArguments: Bundle? = null
val navTargetId
get() = navTarget.id
private val navBackStack = mutableListOf<NavTarget>()
private val navBackStack = mutableListOf<Pair<NavTarget, Bundle?>>()
private var navLoading = true
/* ____ _____ _
@ -258,6 +266,7 @@ class MainActivity : AppCompatActivity() {
Log.d(TAG, Signing.appPassword)
mainSnackbar.setCoordinator(b.navView.coordinator, b.navView.bottomBar)
errorSnackbar.setCoordinator(b.navView.coordinator, b.navView.bottomBar)
navLoading = true
@ -552,7 +561,7 @@ class MainActivity : AppCompatActivity() {
).enqueue(this)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onSyncStartedEvent(event: ApiTaskStartedEvent) {
fun onApiTaskStartedEvent(event: ApiTaskStartedEvent) {
swipeRefreshLayout.isRefreshing = true
if (event.profileId == App.profileId) {
navView.toolbar.apply {
@ -563,7 +572,7 @@ class MainActivity : AppCompatActivity() {
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onSyncProgressEvent(event: ApiTaskProgressEvent) {
fun onApiTaskProgressEvent(event: ApiTaskProgressEvent) {
if (event.profileId == App.profileId) {
navView.toolbar.apply {
subtitleFormat = null
@ -576,8 +585,8 @@ class MainActivity : AppCompatActivity() {
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onSyncProfileFinishedEvent(event: ApiTaskFinishedEvent) {
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onApiTaskFinishedEvent(event: ApiTaskFinishedEvent) {
if (event.profileId == App.profileId) {
navView.toolbar.apply {
subtitleFormat = R.string.toolbar_subtitle
@ -586,17 +595,18 @@ class MainActivity : AppCompatActivity() {
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onSyncFinishedEvent(event: ApiTaskAllFinishedEvent) {
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onApiTaskAllFinishedEvent(event: ApiTaskAllFinishedEvent) {
swipeRefreshLayout.isRefreshing = false
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onSyncErrorEvent(event: ApiTaskErrorEvent) {
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onApiTaskErrorEvent(event: ApiTaskErrorEvent) {
navView.toolbar.apply {
subtitleFormat = R.string.toolbar_subtitle
subtitleFormatWithUnread = R.plurals.toolbar_subtitle_with_unread
subtitle = "Gotowe"
}
mainSnackbar.dismiss()
errorSnackbar.addError(event.error).show()
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
@ -800,6 +810,10 @@ class MainActivity : AppCompatActivity() {
}
AsyncTask.execute {
app.profileLoadById(id)
MessagesFragment.pageSelection = -1
MessagesListFragment.tapPositions = intArrayOf(RecyclerView.NO_POSITION, RecyclerView.NO_POSITION)
MessagesListFragment.topPositions = intArrayOf(RecyclerView.NO_POSITION, RecyclerView.NO_POSITION)
MessagesListFragment.bottomPositions = intArrayOf(RecyclerView.NO_POSITION, RecyclerView.NO_POSITION)
this.runOnUiThread {
if (app.profile == null) {
@ -825,7 +839,7 @@ class MainActivity : AppCompatActivity() {
loadId = DRAWER_ITEM_HOME
}
val target = navTargetList
.singleOrNull { it.id == loadId }
.firstOrNull { it.id == loadId }
if (target == null) {
Toast.makeText(this, getString(R.string.error_invalid_fragment, id), Toast.LENGTH_LONG).show()
loadTarget(navTargetList.first(), arguments)
@ -835,7 +849,7 @@ class MainActivity : AppCompatActivity() {
}
}
private fun loadTarget(target: NavTarget, arguments: Bundle? = null) {
d("NavDebug", "loadItem(id = ${target.id})")
d("NavDebug", "loadTarget(target = $target, arguments = $arguments)")
bottomSheet.close()
bottomSheet.removeAllContextual()
@ -862,7 +876,7 @@ class MainActivity : AppCompatActivity() {
)
}
else {
navBackStack.lastIndexOf(target).let {
navBackStack.keys().lastIndexOf(target).let {
if (it == -1)
return@let target
// pop the back stack up until that target
@ -893,8 +907,9 @@ class MainActivity : AppCompatActivity() {
R.anim.task_open_enter,
R.anim.task_open_exit
)
navBackStack.add(navTarget)
navBackStack.add(navTarget to arguments)
navTarget = target
navArguments = arguments
}
}
@ -909,7 +924,7 @@ class MainActivity : AppCompatActivity() {
d("NavDebug", "Current fragment ${navTarget.fragmentClass?.java?.simpleName}, pop to home ${navTarget.popToHome}, back stack:")
navBackStack.forEachIndexed { index, target2 ->
d("NavDebug", " - $index: ${target2.fragmentClass?.java?.simpleName}")
d("NavDebug", " - $index: ${target2.first.fragmentClass?.java?.simpleName}")
}
transaction.replace(R.id.fragment, fragment)
@ -934,11 +949,18 @@ class MainActivity : AppCompatActivity() {
return false
}
// TODO back stack argument support
if (navTarget.popToHome) {
loadTarget(HOME_ID)
}
else {
loadTarget(navBackStack.last())
when {
navTarget.popToHome -> {
loadTarget(HOME_ID)
}
navTarget.popTo != null -> {
loadTarget(navTarget.popTo ?: HOME_ID)
}
else -> {
navBackStack.last().let {
loadTarget(it.first, it.second)
}
}
}
return true
}
@ -953,6 +975,8 @@ class MainActivity : AppCompatActivity() {
* that something has changed in the bottom sheet.
*/
fun gainAttention() {
if (app.config.ui.bottomSheetOpened || true)
return
b.navView.postDelayed({
navView.gainAttentionOnBottomBar()
}, 2000)
@ -1076,4 +1100,7 @@ class MainActivity : AppCompatActivity() {
}
}
}
fun snackbar(text: String, actionText: String? = null, onClick: (() -> Unit)? = null) = mainSnackbar.snackbar(text, actionText, onClick)
fun snackbarDismiss() = mainSnackbar.dismiss()
}

View File

@ -64,6 +64,11 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
get() { mAppRateSnackbarTime = mAppRateSnackbarTime ?: values.get("appRateSnackbarTime", 0L); return mAppRateSnackbarTime ?: 0L }
set(value) { set("appRateSnackbarTime", value); mAppRateSnackbarTime = value }
private var mRunSync: Boolean? = null
var runSync: Boolean
get() { mRunSync = mRunSync ?: values.get("runSync", false); return mRunSync ?: false }
set(value) { set("runSync", value); mRunSync = value }
private var rawEntries: List<ConfigEntry> = db.configDao().getAllNow()
private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf()
init {
@ -76,6 +81,9 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
fun getFor(profileId: Int): ProfileConfig {
return profileConfigs[profileId] ?: ProfileConfig(db, profileId, rawEntries)
}
fun forProfile(): ProfileConfig {
return profileConfigs[App.profileId] ?: ProfileConfig(db, App.profileId, rawEntries)
}
fun setProfile(profileId: Int) {
}

View File

@ -59,4 +59,9 @@ class ConfigUI(private val config: Config) {
var snowfall: Boolean
get() { mSnowfall = mSnowfall ?: config.values.get("snowfall", false); return mSnowfall ?: false }
set(value) { config.set("snowfall", value); mSnowfall = value }
private var mBottomSheetOpened: Boolean? = null
var bottomSheetOpened: Boolean
get() { mBottomSheetOpened = mBottomSheetOpened ?: config.values.get("bottomSheetOpened", false); return mBottomSheetOpened ?: false }
set(value) { config.set("bottomSheetOpened", value); mBottomSheetOpened = value }
}

View File

@ -44,6 +44,7 @@ class ApiService : Service() {
SzkolnyTask.sync(syncingProfiles),
NotifyTask()
)
private val allTaskList = mutableListOf<IApiTask>()
private val taskQueue = mutableListOf<IApiTask>()
private val errorList = mutableListOf<ApiError>()
@ -73,7 +74,7 @@ class ApiService : Service() {
override fun onCompleted() {
lastEventTime = System.currentTimeMillis()
d(TAG, "Task $taskRunningId (profile $taskProfileId) - $taskProgressText - finished")
EventBus.getDefault().post(ApiTaskFinishedEvent(taskProfileId))
EventBus.getDefault().postSticky(ApiTaskFinishedEvent(taskProfileId))
clearTask()
notification.setIdle().post()
@ -84,7 +85,7 @@ class ApiService : Service() {
lastEventTime = System.currentTimeMillis()
d(TAG, "Task $taskRunningId threw an error - $apiError")
apiError.profileId = taskProfileId
EventBus.getDefault().post(ApiTaskErrorEvent(apiError))
EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError))
errorList.add(apiError)
apiError.throwable?.printStackTrace()
if (apiError.isCritical) {
@ -194,7 +195,7 @@ class ApiService : Service() {
*/
private fun stopIfTaskFrozen() {
if (checkIfTaskFrozen()) {
stopSelf()
allCompleted()
}
}
@ -213,7 +214,7 @@ class ApiService : Service() {
}
private fun allCompleted() {
EventBus.getDefault().post(ApiTaskAllFinishedEvent())
EventBus.getDefault().postSticky(ApiTaskAllFinishedEvent())
stopSelf()
}
@ -228,6 +229,11 @@ class ApiService : Service() {
EventBus.getDefault().removeStickyEvent(task)
d(TAG, task.toString())
// fix for duplicated tasks, thank you EventBus
if (task in allTaskList)
return
allTaskList += task
if (task is EdziennikTask) {
when (task.request) {
is EdziennikTask.SyncRequest -> app.db.profileDao().idsForSyncNow.forEach {
@ -270,7 +276,7 @@ class ApiService : Service() {
serviceClosed = true
taskCancelled = true
taskRunning?.cancel()
stopSelf()
allCompleted()
}
/* _____ _ _ _

View File

@ -70,6 +70,8 @@ const val IDZIENNIK_WEB_ATTENDANCE = "mod_panelRodzica/obecnosci/WS_obecnosciUcz
const val IDZIENNIK_WEB_ANNOUNCEMENTS = "mod_panelRodzica/tabOgl/WS_tablicaOgloszen.asmx/GetOgloszenia"
const val IDZIENNIK_WEB_MESSAGES_LIST = "mod_komunikator/WS_wiadomosci.asmx/PobierzListeWiadomosci"
const val IDZIENNIK_WEB_GET_MESSAGE = "mod_komunikator/WS_wiadomosci.asmx/PobierzWiadomosc"
const val IDZIENNIK_WEB_GET_RECIPIENT_LIST = "mod_komunikator/WS_wiadomosci.asmx/pobierzListeOdbiorcowPanelRodzic"
const val IDZIENNIK_WEB_SEND_MESSAGE = "mod_komunikator/WS_wiadomosci.asmx/WyslijWiadomosc"
const val IDZIENNIK_WEB_GET_ATTACHMENT = "mod_komunikator/Download.ashx"
val IDZIENNIK_API_USER_AGENT = SYSTEM_USER_AGENT
@ -102,6 +104,7 @@ const val VULCAN_API_ENDPOINT_ATTENDANCE = "mobile-api/Uczen.v3.Uczen/Frekwencje
const val VULCAN_API_ENDPOINT_MESSAGES_RECEIVED = "mobile-api/Uczen.v3.Uczen/WiadomosciOdebrane"
const val VULCAN_API_ENDPOINT_MESSAGES_SENT = "mobile-api/Uczen.v3.Uczen/WiadomosciWyslane"
const val VULCAN_API_ENDPOINT_MESSAGES_CHANGE_STATUS = "mobile-api/Uczen.v3.Uczen/ZmienStatusWiadomosci"
const val VULCAN_API_ENDPOINT_MESSAGES_ADD = "mobile-api/Uczen.v3.Uczen/DodajWiadomosc"
const val VULCAN_API_ENDPOINT_PUSH = "mobile-api/Uczen.v3.Uczen/UstawPushToken"
const val EDUDZIENNIK_USER_AGENT = "Szkolny.eu/${BuildConfig.VERSION_NAME}"

View File

@ -81,3 +81,32 @@ fun Data.prepare(loginMethods: List<LoginMethod>, features: List<Feature>, featu
progressCount = targetLoginMethodIds.size + targetEndpointIds.size
progressStep = if (progressCount <= 0) 0f else 100f / progressCount.toFloat()
}
fun Data.prepareFor(loginMethods: List<LoginMethod>, loginMethodId: Int) {
val possibleLoginMethods = this.loginMethods.toMutableList()
loginMethods.forEach {
if (it.isPossible(profile, loginStore))
possibleLoginMethods += it.loginMethodId
}
targetEndpointIds.clear()
targetLoginMethodIds.clear()
// check the login method for any dependencies
var requiredLoginMethod: Int? = loginMethodId
while (requiredLoginMethod != LOGIN_METHOD_NOT_NEEDED) {
loginMethods.singleOrNull { it.loginMethodId == requiredLoginMethod }?.let {
if (requiredLoginMethod != null)
targetLoginMethodIds.add(requiredLoginMethod!!)
requiredLoginMethod = it.requiredLoginMethod(profile, loginStore)
}
}
// sort and distinct every login method
targetLoginMethodIds = targetLoginMethodIds.toHashSet().toMutableList()
targetLoginMethodIds.sort()
progressCount = 0
progressStep = 0f
}

View File

@ -32,6 +32,7 @@ const val CODE_LIBRUS_DISCONNECTED = 31
const val CODE_PROFILE_ARCHIVED = 30*/
const val ERROR_APP_CRASH = 1
const val ERROR_MESSAGE_NOT_SENT = 10
const val ERROR_REQUEST_FAILURE = 50
const val ERROR_REQUEST_HTTP_400 = 51
@ -40,10 +41,13 @@ const val ERROR_REQUEST_HTTP_403 = 53
const val ERROR_REQUEST_HTTP_404 = 54
const val ERROR_REQUEST_HTTP_405 = 55
const val ERROR_REQUEST_HTTP_410 = 56
const val ERROR_REQUEST_HTTP_500 = 57
const val ERROR_REQUEST_HTTP_424 = 57
const val ERROR_REQUEST_HTTP_500 = 58
const val ERROR_REQUEST_HTTP_503 = 59
const val ERROR_REQUEST_FAILURE_HOSTNAME_NOT_FOUND = 60
const val ERROR_REQUEST_FAILURE_TIMEOUT = 61
const val ERROR_REQUEST_FAILURE_NO_INTERNET = 62
const val ERROR_REQUEST_FAILURE_SSL_ERROR = 63
const val ERROR_RESPONSE_EMPTY = 100
const val ERROR_LOGIN_DATA_MISSING = 101
const val ERROR_PROFILE_MISSING = 105
@ -159,6 +163,7 @@ const val ERROR_LOGIN_IDZIENNIK_FIRST_NO_SCHOOL_YEAR = 440
const val ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA = 441
const val ERROR_IDZIENNIK_API_ACCESS_DENIED = 450
const val ERROR_IDZIENNIK_API_OTHER = 451
const val ERROR_IDZIENNIK_API_NO_REGISTER = 452
const val ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN = 501
const val ERROR_LOGIN_EDUDZIENNIK_WEB_OTHER = 510

View File

@ -55,6 +55,10 @@ object Regexes {
"""href="https://.+?\.mobidziennik.pl/.+?&(?:amp;)?zalacznik=([0-9]+)"(?:.+?<small.+?\(([0-9.]+)\s(M|K|G|)B\))*""".toRegex(DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_MESSAGE_RECIPIENTS_JSON by lazy {
"""odbiorcy: (\[.+?\]),${'$'}""".toRegex(RegexOption.MULTILINE)
}
val IDZIENNIK_LOGIN_HIDDEN_FIELDS by lazy {
@ -78,6 +82,9 @@ object Regexes {
val IDZIENNIK_LOGIN_FIRST_STUDENT by lazy {
"""<option.*?value="([0-9]+)"\sdata-id-ucznia="([A-z0-9]+?)".*?>(.+?)\s(.+?)\s*\((.+?),\s*(.+?)\)</option>""".toRegex(DOT_MATCHES_ALL)
}
val IDZIENNIK_MESSAGES_RECIPIENT_PARENT by lazy {
"""(.+?)\s\((.+)\)""".toRegex()
}

View File

@ -147,7 +147,7 @@ class DataEdudziennik(app: App, profile: Profile?, loginStore: LoginStore) : Dat
fun getEventType(longId: String, name: String): EventType {
val id = longId.crc16().toLong()
return eventTypes.singleOrNull { it.id == id } ?: run {
val eventType = EventType(profileId, id, name, colorFromName(app, name))
val eventType = EventType(profileId, id, name, colorFromName(name))
eventTypes.put(id, eventType)
eventType
}

View File

@ -20,6 +20,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.utils.Utils.d
class Edudziennik(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
@ -29,6 +30,7 @@ class Edudziennik(val app: App, val profile: Profile?, val loginStore: LoginStor
val internalErrorList = mutableListOf<Int>()
val data: DataEdudziennik
private var afterLogin: (() -> Unit)? = null
init {
data = DataEdudziennik(app, profile, loginStore).apply {
@ -55,21 +57,17 @@ class Edudziennik(val app: App, val profile: Profile?, val loginStore: LoginStor
override fun sync(featureIds: List<Int>, viewId: Int?, arguments: JsonObject?) {
data.arguments = arguments
data.prepare(edudziennikLoginMethods, EdudziennikFeatures, featureIds, viewId)
d(TAG, "LoginMethod IDs: ${data.targetLoginMethodIds}")
d(TAG, "Endpoint IDs: ${data.targetEndpointIds}")
EdudziennikLogin(data) {
EdudziennikData(data) {
completed()
}
}
login()
}
private fun login() {
private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) {
d(TAG, "Trying to login with ${data.targetLoginMethodIds}")
if (internalErrorList.isNotEmpty()) {
d(TAG, " - Internal errors:")
internalErrorList.forEach { d(TAG, " - code $it") }
}
loginMethodId?.let { data.prepareFor(edudziennikLoginMethods, it) }
afterLogin?.let { this.afterLogin = it }
EdudziennikLogin(data) {
data()
}
@ -81,18 +79,14 @@ class Edudziennik(val app: App, val profile: Profile?, val loginStore: LoginStor
d(TAG, " - Internal errors:")
internalErrorList.forEach { d(TAG, " - code $it") }
}
EdudziennikData(data) {
afterLogin?.invoke() ?: EdudziennikData(data) {
completed()
}
}
override fun getMessage(message: MessageFull) {
}
override fun markAllAnnouncementsAsRead() {
}
override fun getMessage(message: MessageFull) {}
override fun sendMessage(recipients: List<Teacher>, subject: String, text: String) {}
override fun markAllAnnouncementsAsRead() {}
override fun getAnnouncement(announcement: AnnouncementFull) {
EdudziennikLoginWeb(data) {
@ -102,16 +96,10 @@ class Edudziennik(val app: App, val profile: Profile?, val loginStore: LoginStor
}
}
override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) {
}
override fun firstLogin() {
EdudziennikFirstLogin(data) {
completed()
}
}
override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) {}
override fun getRecipientList() {}
override fun firstLogin() { EdudziennikFirstLogin(data) { completed() } }
override fun cancel() {
d(TAG, "Cancelled")
data.cancel()
@ -119,18 +107,9 @@ class Edudziennik(val app: App, val profile: Profile?, val loginStore: LoginStor
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() {
callback.onCompleted()
}
override fun onProgress(step: Float) {
callback.onProgress(step)
}
override fun onStartProgress(stringRes: Int) {
callback.onStartProgress(stringRes)
}
override fun onCompleted() { callback.onCompleted() }
override fun onProgress(step: Float) { callback.onProgress(step) }
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
override fun onError(apiError: ApiError) {
if (apiError.errorCode in internalErrorList) {
// finish immediately if the same error occurs twice during the same sync

View File

@ -6,23 +6,23 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.CODE_INTERNAL_LIBRUS_ACCOUNT_410
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikData
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web.IdziennikWebGetAttachment
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web.IdziennikWebGetMessage
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web.IdziennikWebGetRecipientList
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web.IdziennikWebSendMessage
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.firstlogin.IdziennikFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.login.IdziennikLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.login.IdziennikLoginWeb
import pl.szczodrzynski.edziennik.data.api.idziennikLoginMethods
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.prepare
import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.utils.Utils.d
class Idziennik(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
@ -32,6 +32,7 @@ class Idziennik(val app: App, val profile: Profile?, val loginStore: LoginStore,
val internalErrorList = mutableListOf<Int>()
val data: DataIdziennik
private var afterLogin: (() -> Unit)? = null
init {
data = DataIdziennik(app, profile, loginStore).apply {
@ -58,45 +59,69 @@ class Idziennik(val app: App, val profile: Profile?, val loginStore: LoginStore,
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}")
login()
}
private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) {
d(TAG, "Trying to login with ${data.targetLoginMethodIds}")
if (internalErrorList.isNotEmpty()) {
d(TAG, " - Internal errors:")
internalErrorList.forEach { d(TAG, " - code $it") }
}
loginMethodId?.let { data.prepareFor(idziennikLoginMethods, it) }
afterLogin?.let { this.afterLogin = it }
IdziennikLogin(data) {
IdziennikData(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") }
}
afterLogin?.invoke() ?: IdziennikData(data) {
completed()
}
}
override fun getMessage(message: MessageFull) {
IdziennikLoginWeb(data) {
login(LOGIN_METHOD_IDZIENNIK_WEB) {
IdziennikWebGetMessage(data, message) {
completed()
}
}
}
override fun markAllAnnouncementsAsRead() {
override fun sendMessage(recipients: List<Teacher>, subject: String, text: String) {
login(LOGIN_METHOD_IDZIENNIK_API) {
IdziennikWebSendMessage(data, recipients, subject, text) {
completed()
}
}
}
override fun getAnnouncement(announcement: AnnouncementFull) {
}
override fun markAllAnnouncementsAsRead() {}
override fun getAnnouncement(announcement: AnnouncementFull) {}
override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) {
IdziennikLoginWeb(data) {
login(LOGIN_METHOD_IDZIENNIK_WEB) {
IdziennikWebGetAttachment(data, message, attachmentId, attachmentName) {
completed()
}
}
}
override fun firstLogin() {
IdziennikFirstLogin(data) {
completed()
override fun getRecipientList() {
login(LOGIN_METHOD_IDZIENNIK_WEB) {
IdziennikWebGetRecipientList(data) {
completed()
}
}
}
override fun firstLogin() { IdziennikFirstLogin(data) { completed() } }
override fun cancel() {
d(TAG, "Cancelled")
data.cancel()
@ -104,28 +129,29 @@ class Idziennik(val app: App, val profile: Profile?, val loginStore: LoginStore,
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() {
callback.onCompleted()
}
override fun onProgress(step: Float) {
callback.onProgress(step)
}
override fun onStartProgress(stringRes: Int) {
callback.onStartProgress(stringRes)
}
override fun onCompleted() { callback.onCompleted() }
override fun onProgress(step: Float) { callback.onProgress(step) }
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
override fun onError(apiError: ApiError) {
if (apiError.errorCode in internalErrorList) {
// finish immediately if the same error occurs twice during the same sync
callback.onError(apiError)
return
}
internalErrorList.add(apiError.errorCode)
when (apiError.errorCode) {
in internalErrorList -> {
// finish immediately if the same error occurs twice during the same sync
callback.onError(apiError)
ERROR_LOGIN_IDZIENNIK_WEB_NO_SESSION,
ERROR_LOGIN_IDZIENNIK_WEB_NO_AUTH,
ERROR_LOGIN_IDZIENNIK_WEB_NO_BEARER,
ERROR_IDZIENNIK_WEB_ACCESS_DENIED,
ERROR_IDZIENNIK_API_ACCESS_DENIED -> {
data.loginMethods.remove(LOGIN_METHOD_IDZIENNIK_WEB)
data.prepareFor(idziennikLoginMethods, LOGIN_METHOD_IDZIENNIK_WEB)
data.loginExpiryTime = 0
login()
}
CODE_INTERNAL_LIBRUS_ACCOUNT_410 -> {
internalErrorList.add(apiError.errorCode)
loginStore.removeLoginData("refreshToken") // force a clean login
//loginLibrus()
ERROR_IDZIENNIK_API_NO_REGISTER -> {
data()
}
else -> callback.onError(apiError)
}

View File

@ -55,6 +55,7 @@ open class IdziennikApi(open val data: DataIdziennik) {
}
error?.let { code ->
when (code) {
"Uczeń nie posiada aktywnej pozycji w dzienniku" -> ERROR_IDZIENNIK_API_NO_REGISTER
"Authorization has been denied for this request." -> ERROR_IDZIENNIK_API_ACCESS_DENIED
else -> ERROR_IDZIENNIK_API_OTHER
}.let { errorCode ->
@ -107,6 +108,7 @@ open class IdziennikApi(open val data: DataIdziennik) {
}
}
}
.allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST)
.allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED)
.allowErrorCode(HttpURLConnection.HTTP_INTERNAL_ERROR)
.callback(callback)

View File

@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_DELETED
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.getBoolean
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.Utils.crc32
@ -51,11 +52,18 @@ class IdziennikApiMessagesInbox(override val data: DataIdziennik,
val sentDate = Date.fromIso(jMessage.getString("dataWyslania"))
val sender = jMessage.getAsJsonObject("nadawca")
var firstName = sender.getString("imie")
var lastName = sender.getString("nazwisko")
if (firstName.isNullOrEmpty() || lastName.isNullOrEmpty()) {
firstName = "usunięty"
lastName = "użytkownik"
}
val rTeacher = data.getTeacher(
sender.getString("imie") ?: "",
sender.getString("nazwisko") ?: ""
firstName,
lastName
)
rTeacher.loginId = sender.getString("id") + ":" + sender.getString("usr")
rTeacher.loginId = /*sender.getString("id") + ":" + */sender.getString("usr")
rTeacher.setTeacherType(Teacher.TYPE_OTHER)
val message = Message(
profileId,

View File

@ -7,11 +7,11 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.api
import com.google.gson.JsonArray
import pl.szczodrzynski.edziennik.DAY
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES
import pl.szczodrzynski.edziennik.asJsonObjectList
import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_API_MESSAGES_SENT
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.ENDPOINT_IDZIENNIK_API_MESSAGES_SENT
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikApi
import pl.szczodrzynski.edziennik.asJsonObjectList
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
@ -62,7 +62,7 @@ class IdziennikApiMessagesSent(override val data: DataIdziennik,
lastName = "użytkownik"
}
val rTeacher = data.getTeacher(firstName, lastName)
rTeacher.loginId = recipient.get("id").asString + ":" + recipient.get("usr").asString
rTeacher.loginId = /*recipient.get("id").asString + ":" + */recipient.get("usr").asString
val messageRecipient = MessageRecipient(
profileId,

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-30.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web
import androidx.room.OnConflictStrategy
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA
import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_GET_RECIPIENT_LIST
import pl.szczodrzynski.edziennik.data.api.Regexes
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb
import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
class IdziennikWebGetRecipientList(
override val data: DataIdziennik, val onSuccess: () -> Unit) : IdziennikWeb(data) {
companion object {
private const val TAG = "IdziennikWebGetRecipientList"
}
init {
webApiGet(TAG, IDZIENNIK_WEB_GET_RECIPIENT_LIST, mapOf(
"idP" to data.registerId
)) { result ->
val json = result.getJsonObject("d") ?: run {
data.error(ApiError(TAG, ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA)
.withApiResponse(result))
return@webApiGet
}
json.getJsonArray("ListK_Pracownicy")?.asJsonObjectList()?.forEach { recipient ->
val name = recipient.getString("ImieNazwisko") ?: ": "
val (fullName, subject) = name.split(": ").let {
Pair(it.getOrNull(0), it.getOrNull(1))
}
val guid = recipient.getString("Id") ?: ""
// get teacher by ID or create it
val teacher = data.getTeacherByFirstLast(fullName ?: " ")
teacher.loginId = guid
teacher.setTeacherType(Teacher.TYPE_TEACHER)
// unset OTHER that is automatically set in IdziennikApiMessages*
teacher.unsetTeacherType(Teacher.TYPE_OTHER)
teacher.typeDescription = subject
}
json.getJsonArray("ListK_Opiekunowie")?.asJsonObjectList()?.forEach { recipient ->
val name = recipient.getString("ImieNazwisko") ?: ": "
val (fullName, parentOf) = Regexes.IDZIENNIK_MESSAGES_RECIPIENT_PARENT.find(name)?.let {
Pair(it.groupValues.getOrNull(1), it.groupValues.getOrNull(2))
} ?: Pair(null, null)
val guid = recipient.getString("Id") ?: ""
// get teacher by ID or create it
val teacher = data.getTeacherByFirstLast(fullName ?: " ")
teacher.loginId = guid
teacher.setTeacherType(Teacher.TYPE_PARENT)
// unset OTHER that is automatically set in IdziennikApiMessages*
teacher.unsetTeacherType(Teacher.TYPE_OTHER)
teacher.typeDescription = parentOf
}
val event = RecipientListGetEvent(
data.profileId,
data.teacherList.filter { it.loginId != null }
)
profile?.lastReceiversSync = System.currentTimeMillis()
data.teacherOnConflictStrategy = OnConflictStrategy.REPLACE
EventBus.getDefault().postSticky(event)
onSuccess()
}
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-30.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.web
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA
import pl.szczodrzynski.edziennik.data.api.IDZIENNIK_WEB_SEND_MESSAGE
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.IdziennikWeb
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.data.api.IdziennikApiMessagesSent
import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import java.util.*
class IdziennikWebSendMessage(
override val data: DataIdziennik,
val recipients: List<Teacher>,
val subject: String,
val text: String,
val onSuccess: () -> Unit
) : IdziennikWeb(data) {
companion object {
private const val TAG = "IdziennikWebSendMessage"
}
init {
val recipientsArray = JsonArray()
for (teacher in recipients) {
teacher.loginId?.let {
recipientsArray += it
}
}
webApiGet(TAG, IDZIENNIK_WEB_SEND_MESSAGE, mapOf(
"Wiadomosc" to JsonObject(
"Tytul" to subject,
"Tresc" to text,
"Confirmation" to false,
"GuidMessage" to UUID.randomUUID().toString().toUpperCase(Locale.ROOT),
"Odbiorcy" to recipientsArray
)
)) { result ->
val json = result.getJsonObject("d") ?: run {
data.error(ApiError(TAG, ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA)
.withApiResponse(result))
return@webApiGet
}
if (json.getBoolean("CzyJestBlad") != false) {
// TODO error
return@webApiGet
}
IdziennikApiMessagesSent(data) {
val message = data.messageIgnoreList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject }
val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id }
val event = MessageSentEvent(data.profileId, message, metadata?.addedDate)
EventBus.getDefault().postSticky(event)
onSuccess()
}
}
}
}

View File

@ -11,9 +11,11 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusData
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api.LibrusApiAnnouncementMarkAsRead
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesGetAttachment
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesGetMessage
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesGetRecipientList
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesSendMessage
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.LibrusSynergiaMarkAllAnnouncementsAsRead
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.firstlogin.LibrusFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLogin
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -22,6 +24,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.utils.Utils.d
class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
@ -31,6 +34,7 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
val internalErrorList = mutableListOf<Int>()
val data: DataLibrus
private var afterLogin: (() -> Unit)? = null
init {
data = DataLibrus(app, profile, loginStore).apply {
@ -60,12 +64,14 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
login()
}
private fun login() {
private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) {
d(TAG, "Trying to login with ${data.targetLoginMethodIds}")
if (internalErrorList.isNotEmpty()) {
d(TAG, " - Internal errors:")
internalErrorList.forEach { d(TAG, " - code $it") }
}
loginMethodId?.let { data.prepareFor(librusLoginMethods, it) }
afterLogin?.let { this.afterLogin = it }
LibrusLogin(data) {
data()
}
@ -77,67 +83,60 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
d(TAG, " - Internal errors:")
internalErrorList.forEach { d(TAG, " - code $it") }
}
LibrusData(data) {
afterLogin?.invoke() ?: LibrusData(data) {
completed()
}
}
override fun getMessage(message: MessageFull) {
LibrusLoginPortal(data) {
LibrusLoginApi(data) {
LibrusLoginSynergia(data) {
LibrusLoginMessages(data) {
LibrusMessagesGetMessage(data, message) {
completed()
}
}
}
login(LOGIN_METHOD_LIBRUS_MESSAGES) {
LibrusMessagesGetMessage(data, message) {
completed()
}
}
}
override fun sendMessage(recipients: List<Teacher>, subject: String, text: String) {
login(LOGIN_METHOD_LIBRUS_MESSAGES) {
LibrusMessagesSendMessage(data, recipients, subject, text) {
completed()
}
}
}
override fun markAllAnnouncementsAsRead() {
LibrusLoginPortal(data) {
LibrusLoginApi(data) {
LibrusLoginSynergia(data) {
LibrusSynergiaMarkAllAnnouncementsAsRead(data) {
completed()
}
}
login(LOGIN_METHOD_LIBRUS_SYNERGIA) {
LibrusSynergiaMarkAllAnnouncementsAsRead(data) {
completed()
}
}
}
override fun getAnnouncement(announcement: AnnouncementFull) {
LibrusLoginPortal(data) {
LibrusLoginApi(data) {
LibrusApiAnnouncementMarkAsRead(data, announcement) {
completed()
}
login(LOGIN_METHOD_LIBRUS_API) {
LibrusApiAnnouncementMarkAsRead(data, announcement) {
completed()
}
}
}
override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) {
LibrusLoginPortal(data) {
LibrusLoginApi(data) {
LibrusLoginSynergia(data) {
LibrusLoginMessages(data) {
LibrusMessagesGetAttachment(data, message, attachmentId, attachmentName) {
completed()
}
}
}
login(LOGIN_METHOD_LIBRUS_MESSAGES) {
LibrusMessagesGetAttachment(data, message, attachmentId, attachmentName) {
completed()
}
}
}
override fun firstLogin() {
LibrusFirstLogin(data) {
completed()
override fun getRecipientList() {
login(LOGIN_METHOD_LIBRUS_MESSAGES) {
LibrusMessagesGetRecipientList(data) {
completed()
}
}
}
override fun firstLogin() { LibrusFirstLogin(data) { completed() } }
override fun cancel() {
d(TAG, "Cancelled")
data.cancel()
@ -145,18 +144,9 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() {
callback.onCompleted()
}
override fun onProgress(step: Float) {
callback.onProgress(step)
}
override fun onStartProgress(stringRes: Int) {
callback.onStartProgress(stringRes)
}
override fun onCompleted() { callback.onCompleted() }
override fun onProgress(step: Float) { callback.onProgress(step) }
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
override fun onError(apiError: ApiError) {
if (apiError.errorCode in internalErrorList) {
// finish immediately if the same error occurs twice during the same sync
@ -167,30 +157,26 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
when (apiError.errorCode) {
ERROR_LIBRUS_PORTAL_ACCESS_DENIED -> {
data.loginMethods.remove(LOGIN_METHOD_LIBRUS_PORTAL)
data.targetLoginMethodIds.add(LOGIN_METHOD_LIBRUS_PORTAL)
data.targetLoginMethodIds.sort()
data.prepareFor(librusLoginMethods, LOGIN_METHOD_LIBRUS_PORTAL)
data.portalTokenExpiryTime = 0
login()
}
ERROR_LIBRUS_API_ACCESS_DENIED,
ERROR_LIBRUS_API_TOKEN_EXPIRED -> {
data.loginMethods.remove(LOGIN_METHOD_LIBRUS_API)
data.targetLoginMethodIds.add(LOGIN_METHOD_LIBRUS_API)
data.targetLoginMethodIds.sort()
data.prepareFor(librusLoginMethods, LOGIN_METHOD_LIBRUS_API)
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.prepareFor(librusLoginMethods, LOGIN_METHOD_LIBRUS_SYNERGIA)
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.prepareFor(librusLoginMethods, LOGIN_METHOD_LIBRUS_MESSAGES)
data.messagesSessionIdExpiryTime = 0
login()
}

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.body.MediaTypeUtils
@ -12,6 +13,8 @@ import im.wangchao.mhttp.callback.FileCallbackHandler
import im.wangchao.mhttp.callback.JsonCallbackHandler
import im.wangchao.mhttp.callback.TextCallbackHandler
import okhttp3.Cookie
import org.json.JSONObject
import org.json.XML
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.parser.Parser
@ -38,10 +41,10 @@ open class LibrusMessages(open val data: DataLibrus) {
val profile
get() = data.profile
fun messagesGet(tag: String, endpoint: String, method: Int = POST,
fun messagesGet(tag: String, module: String, method: Int = POST,
parameters: Map<String, Any>? = null, onSuccess: (doc: Document) -> Unit) {
d(tag, "Request: Librus/Messages - $LIBRUS_MESSAGES_URL/$endpoint")
d(tag, "Request: Librus/Messages - $LIBRUS_MESSAGES_URL/$module")
val callback = object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
@ -107,22 +110,92 @@ open class LibrusMessages(open val data: DataLibrus) {
transformer.transform(DOMSource(doc), StreamResult(stringWriter))
val requestXml = stringWriter.toString()
/*val requestXml = xml("service") {
"header" { }
"data" {
for ((key, value) in parameters.orEmpty()) {
key {
-value.toString()
Request.builder()
.url("$LIBRUS_MESSAGES_URL/$module")
.userAgent(SYNERGIA_USER_AGENT)
.setTextBody(requestXml, MediaTypeUtils.APPLICATION_XML)
.apply {
when (method) {
GET -> get()
POST -> post()
}
}
.callback(callback)
.build()
.enqueue()
}
fun messagesGetJson(tag: String, module: String, method: Int = POST,
parameters: Map<String, Any>? = null, onSuccess: (json: JsonObject?) -> Unit) {
d(tag, "Request: Librus/Messages - $LIBRUS_MESSAGES_URL/$module")
val callback = object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
if (text.isNullOrEmpty()) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
return
}
when {
text.contains("<message>Niepoprawny login i/lub hasło.</message>") -> data.error(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN, response, text)
text.contains("stop.png") -> data.error(TAG, ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED, response, text)
text.contains("eAccessDeny") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text.contains("OffLine") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_MAINTENANCE, response, text)
text.contains("<status>error</status>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ERROR, response, text)
text.contains("<type>eVarWhitThisNameNotExists</type>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text.contains("<error>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_OTHER, response, text)
}
try {
val json: JSONObject? = XML.toJSONObject(text)
onSuccess(JsonParser().parse(json?.toString() ?: "{}")?.asJsonObject)
} catch (e: Exception) {
data.error(ApiError(tag, EXCEPTION_LIBRUS_MESSAGES_REQUEST)
.withResponse(response)
.withThrowable(e)
.withApiResponse(text))
}
}
}.toString(PrintOptions(
singleLineTextElements = true,
useSelfClosingTags = true
))*/
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(tag, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
data.app.cookieJar.saveFromResponse(null, listOf(
Cookie.Builder()
.name("DZIENNIKSID")
.value(data.messagesSessionId!!)
.domain("wiadomosci.librus.pl")
.secure().httpOnly().build()
))
val docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
val doc = docBuilder.newDocument()
val serviceElement = doc.createElement("service")
val headerElement = doc.createElement("header")
val dataElement = doc.createElement("data")
for ((key, value) in parameters.orEmpty()) {
val element = doc.createElement(key)
element.appendChild(doc.createTextNode(value.toString()))
dataElement.appendChild(element)
}
serviceElement.appendChild(headerElement)
serviceElement.appendChild(dataElement)
doc.appendChild(serviceElement)
val transformer = TransformerFactory.newInstance().newTransformer()
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
val stringWriter = StringWriter()
transformer.transform(DOMSource(doc), StreamResult(stringWriter))
val requestXml = stringWriter.toString()
Request.builder()
.url("$LIBRUS_MESSAGES_URL/$endpoint")
.url("$LIBRUS_MESSAGES_URL/$module")
.userAgent(SYNERGIA_USER_AGENT)
.setTextBody(requestXml, MediaTypeUtils.APPLICATION_XML)
.apply {

View File

@ -25,7 +25,14 @@ class LibrusApiUsers(override val data: DataLibrus,
val firstName = user.getString("FirstName")?.fixName() ?: ""
val lastName = user.getString("LastName")?.fixName() ?: ""
data.teacherList.put(id, Teacher(profileId, id, firstName, lastName))
val teacher = Teacher(profileId, id, firstName, lastName)
if (user.getBoolean("IsSchoolAdministrator") == true)
teacher.setTeacherType(Teacher.TYPE_SCHOOL_ADMIN)
if (user.getBoolean("IsPedagogue") == true)
teacher.setTeacherType(Teacher.TYPE_PEDAGOGUE)
data.teacherList.put(id, teacher)
}
data.setSyncNext(ENDPOINT_LIBRUS_API_USERS, 4*DAY)

View File

@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.fixName
import pl.szczodrzynski.edziennik.singleOrNull
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.models.Date
@ -51,12 +52,12 @@ class LibrusMessagesGetList(override val data: DataLibrus, private val type: Int
val recipientFirstName = element.select(when (type) {
TYPE_RECEIVED -> "senderFirstName"
else -> "receiverFirstName"
}).text().trim()
}).text().fixName()
val recipientLastName = element.select(when (type) {
TYPE_RECEIVED -> "senderLastName"
else -> "receiverLastName"
}).text().trim()
}).text().fixName()
val recipientId = data.teacherList.singleOrNull {
it.name == recipientFirstName && it.surname == recipientLastName

View File

@ -14,7 +14,10 @@ import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipientFull
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.fixName
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.notEmptyOrNull
import pl.szczodrzynski.edziennik.singleOrNull
import pl.szczodrzynski.edziennik.utils.models.Date
import java.nio.charset.Charset
@ -56,11 +59,45 @@ class LibrusMessagesGetMessage(
when (messageObject.type) {
TYPE_RECEIVED -> {
val senderLoginId = message.select("senderId").text()
data.teacherList.singleOrNull { it.id == messageObject.senderId }?.loginId = senderLoginId
val senderLoginId = message.select("senderId").text().notEmptyOrNull()
val senderGroupId = message.select("senderGroupId").text().toIntOrNull()
val userClass = message.select("userClass").text().notEmptyOrNull()
data.teacherList.singleOrNull { it.id == messageObject.senderId }?.apply {
loginId = senderLoginId
setTeacherType(when (senderGroupId) {
/* https://api.librus.pl/2.0/Messages/Role */
0, 1, 99 -> Teacher.TYPE_SUPER_ADMIN
2 -> Teacher.TYPE_SCHOOL_ADMIN
3 -> Teacher.TYPE_PRINCIPAL
4 -> Teacher.TYPE_TEACHER
5, 9 -> {
if (typeDescription == null)
typeDescription = userClass
Teacher.TYPE_PARENT
}
7 -> Teacher.TYPE_SECRETARIAT
8 -> {
if (typeDescription == null)
typeDescription = userClass
Teacher.TYPE_STUDENT
}
10 -> Teacher.TYPE_PEDAGOGUE
11 -> Teacher.TYPE_LIBRARIAN
12 -> Teacher.TYPE_SPECIALIST
21 -> {
typeDescription = "Jednostka Nadrzędna"
Teacher.TYPE_OTHER
}
50 -> {
typeDescription = "Jednostka Samorządu Terytorialnego"
Teacher.TYPE_OTHER
}
else -> Teacher.TYPE_OTHER
})
}
val readDateText = message.select("readDate").text()
val readDate = when (readDateText.isNotEmpty()) {
val readDate = when (readDateText.isNotNullNorEmpty()) {
true -> Date.fromIso(readDateText)
else -> 0
}
@ -73,7 +110,7 @@ class LibrusMessagesGetMessage(
messageObject.id
)
messageRecipientObject.fullName = profile.accountNameLong ?: profile.studentNameLong
messageRecipientObject.fullName = profile.accountNameLong ?: profile.studentNameLong ?: ""
messageRecipientList.add(messageRecipientObject)
}
@ -90,7 +127,7 @@ class LibrusMessagesGetMessage(
teacher?.loginId = receiverLoginId
val readDateText = message.select("readed").text()
val readDate = when (readDateText.isNotEmpty()) {
val readDate = when (readDateText.isNotNullNorEmpty()) {
true -> Date.fromIso(readDateText)
else -> 0
}

View File

@ -0,0 +1,174 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-31.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages
import androidx.core.util.set
import androidx.room.OnConflictStrategy
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages
import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
class LibrusMessagesGetRecipientList(
override val data: DataLibrus, val onSuccess: () -> Unit) : LibrusMessages(data) {
companion object {
private const val TAG = "LibrusMessagesGetRecipientList"
}
private val listTypes = mutableListOf<Pair<String, String>>()
init {
messagesGet(TAG, "Receivers/action/GetTypes", parameters = mapOf(
"includeClass" to 1
)) { doc ->
doc.select("response GetTypes data list ArrayItem")?.forEach {
val id = it.getElementsByTag("id")?.firstOrNull()?.ownText() ?: return@forEach
val name = it.getElementsByTag("name")?.firstOrNull()?.ownText() ?: return@forEach
listTypes += id to name
}
getLists()
}
}
private fun getLists() {
if (listTypes.isEmpty()) {
finish()
return
}
val type = listTypes.removeAt(0)
if (type.first == "contactsGroups") {
getLists()
return
}
messagesGetJson(TAG, "Receivers/action/GetListForType", parameters = mapOf(
"receiverType" to type.first
)) { json ->
val dataEl = json?.getJsonObject("response")?.getJsonObject("GetListForType")?.get("data")
if (dataEl is JsonObject) {
val listEl = dataEl.get("ArrayItem")
if (listEl is JsonArray) {
listEl.asJsonObjectList()?.forEach { item ->
processElement(item, type.first, type.second)
}
}
if (listEl is JsonObject) {
processElement(listEl, type.first, type.second)
}
}
getLists()
}
}
private fun processElement(element: JsonObject, typeId: String, typeName: String, listName: String? = null) {
val listEl = element.getJsonObject("list")?.get("ArrayItem")
if (listEl is JsonArray) {
listEl.asJsonObjectList()?.let { list ->
val label = element.getString("label") ?: ""
list.forEach { item ->
processElement(item, typeId, typeName, label)
}
return
}
}
if (listEl is JsonObject) {
val label = element.getString("label") ?: ""
processElement(listEl, typeId, typeName, label)
return
}
processRecipient(element, typeId, typeName, listName)
}
private fun processRecipient(recipient: JsonObject, typeId: String, typeName: String, listName: String? = null) {
val id = recipient.getLong("id") ?: return
val label = recipient.getString("label") ?: return
val fullNameLastFirst: String
val description: String?
if (typeId == "parentsCouncil" || typeId == "schoolParentsCouncil") {
val delimiterIndex = label.lastIndexOf(" - ")
if (delimiterIndex == -1) {
fullNameLastFirst = label.fixName()
description = null
}
else {
fullNameLastFirst = label.substring(0, delimiterIndex).fixName()
description = label.substring(delimiterIndex+3)
}
}
else {
fullNameLastFirst = label.fixName()
description = null
}
var typeDescription: String? = null
val type = when (typeId) {
"tutors" -> Teacher.TYPE_EDUCATOR
"teachers" -> Teacher.TYPE_TEACHER
"classParents" -> Teacher.TYPE_PARENT
"guardians" -> Teacher.TYPE_PARENT
"parentsCouncil" -> {
typeDescription = joinNotNullStrings(": ", listName, description)
Teacher.TYPE_PARENTS_COUNCIL
}
"schoolParentsCouncil" -> {
typeDescription = joinNotNullStrings(": ", listName, description)
Teacher.TYPE_SCHOOL_PARENTS_COUNCIL
}
"pedagogue" -> Teacher.TYPE_PEDAGOGUE
"librarian" -> Teacher.TYPE_LIBRARIAN
"admin" -> Teacher.TYPE_SCHOOL_ADMIN
"secretary" -> Teacher.TYPE_SECRETARIAT
"sadmin" -> Teacher.TYPE_SUPER_ADMIN
else -> {
typeDescription = typeName
Teacher.TYPE_OTHER
}
}
// get teacher by fullName AND type or create it
val teacher = data.teacherList.singleOrNull {
it.fullNameLastFirst == fullNameLastFirst && ((type != Teacher.TYPE_SCHOOL_ADMIN && type != Teacher.TYPE_PEDAGOGUE) || it.isType(type))
} ?: Teacher(data.profileId, id).apply {
if (typeId == "sadmin" && id == 2L) {
name = "Pomoc"
surname = "Techniczna LIBRUS"
}
else {
name = fullNameLastFirst
fullNameLastFirst.splitName()?.let {
name = it.second
surname = it.first
}
}
data.teacherList[id] = this
}
teacher.apply {
this.loginId = id.toString()
this.setTeacherType(type)
if (this.typeDescription.isNullOrBlank())
this.typeDescription = typeDescription
}
}
private fun finish() {
val event = RecipientListGetEvent(
data.profileId,
data.teacherList.filter { it.loginId != null }
)
profile?.lastReceiversSync = System.currentTimeMillis()
data.teacherOnConflictStrategy = OnConflictStrategy.REPLACE
EventBus.getDefault().postSticky(event)
onSuccess()
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-2.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.base64Encode
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages
import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.getJsonObject
import pl.szczodrzynski.edziennik.getLong
import pl.szczodrzynski.edziennik.getString
class LibrusMessagesSendMessage(
override val data: DataLibrus,
val recipients: List<Teacher>,
val subject: String,
val text: String,
val onSuccess: () -> Unit
) : LibrusMessages(data) {
companion object {
const val TAG = "LibrusMessages"
}
init {
val params = mapOf<String, Any>(
"topic" to subject.base64Encode(),
"message" to text.base64Encode(),
"receivers" to recipients
.filter { it.loginId != null }
.joinToString(",") { it.loginId ?: "" },
"actions" to "<Actions/>".base64Encode()
)
messagesGetJson(TAG, "SendMessage", parameters = params) { json ->
val response = json.getJsonObject("response").getJsonObject("SendMessage")
val id = response.getLong("data")
if (response.getString("status") != "ok" || id == null) {
val message = response.getString("message")
// TODO error
return@messagesGetJson
}
LibrusMessagesGetList(data, type = Message.TYPE_SENT) {
val message = data.messageIgnoreList.firstOrNull { it.type == Message.TYPE_SENT && it.id == id }
val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id }
val event = MessageSentEvent(data.profileId, message, metadata?.addedDate)
EventBus.getDefault().postSticky(event)
onSuccess()
}
}
}
}

View File

@ -2,10 +2,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.firstlogin
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_NO_STUDENTS_IN_ACCOUNT
import pl.szczodrzynski.edziennik.data.api.FAKE_LIBRUS_ACCOUNTS
import pl.szczodrzynski.edziennik.data.api.LIBRUS_ACCOUNTS_URL
import pl.szczodrzynski.edziennik.data.api.LOGIN_MODE_LIBRUS_EMAIL
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusPortal
@ -13,8 +10,6 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginPortal
import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.models.AppError.CODE_LIBRUS_DISCONNECTED
import pl.szczodrzynski.edziennik.data.api.models.AppError.CODE_SYNERGIA_NOT_ACTIVATED
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) {
@ -46,8 +41,8 @@ class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) {
val state = account.getString("state")
when (state) {
"requiring_an_action" -> CODE_LIBRUS_DISCONNECTED
"need-activation" -> CODE_SYNERGIA_NOT_ACTIVATED
"requiring_an_action" -> ERROR_LIBRUS_PORTAL_SYNERGIA_DISCONNECTED
"need-activation" -> ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED
else -> null
}?.let { errorCode ->
data.error(ApiError(TAG, errorCode)

View File

@ -59,7 +59,7 @@ class SynergiaTokenExtractor(override val data: DataLibrus, val onSuccess: () ->
data.apiTokenExpiryTime = response.getUnixDate() + 6 * 60 * 60
// TODO remove this
data.profile?.studentNameLong = json.getString("studentName")
data.profile?.studentNameLong = json.getString("studentName") ?: ""
val nameParts = json.getString("studentName")?.split(" ")
data.profile?.studentNameShort = nameParts?.get(0) + " " + nameParts?.get(1)?.get(0)

View File

@ -6,23 +6,23 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.CODE_INTERNAL_LIBRUS_ACCOUNT_410
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikData
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebGetAttachment
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebGetMessage
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebGetRecipientList
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebSendMessage
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.firstlogin.MobidziennikFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLoginWeb
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.mobidziennikLoginMethods
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.prepare
import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.utils.Utils.d
class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
@ -34,6 +34,7 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto
val internalErrorList = mutableListOf<Int>()
val data: DataMobidziennik
private var afterLogin: (() -> Unit)? = null
init {
data = DataMobidziennik(app, profile, loginStore).apply {
@ -60,45 +61,69 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto
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}")
login()
}
private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) {
d(TAG, "Trying to login with ${data.targetLoginMethodIds}")
if (internalErrorList.isNotEmpty()) {
d(TAG, " - Internal errors:")
internalErrorList.forEach { d(TAG, " - code $it") }
}
loginMethodId?.let { data.prepareFor(mobidziennikLoginMethods, it) }
afterLogin?.let { this.afterLogin = it }
MobidziennikLogin(data) {
MobidziennikData(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") }
}
afterLogin?.invoke() ?: MobidziennikData(data) {
completed()
}
}
override fun getMessage(message: MessageFull) {
MobidziennikLoginWeb(data) {
login(LOGIN_METHOD_MOBIDZIENNIK_WEB) {
MobidziennikWebGetMessage(data, message) {
completed()
}
}
}
override fun markAllAnnouncementsAsRead() {
override fun sendMessage(recipients: List<Teacher>, subject: String, text: String) {
login(LOGIN_METHOD_MOBIDZIENNIK_WEB) {
MobidziennikWebSendMessage(data, recipients, subject, text) {
completed()
}
}
}
override fun getAnnouncement(announcement: AnnouncementFull) {
}
override fun markAllAnnouncementsAsRead() {}
override fun getAnnouncement(announcement: AnnouncementFull) {}
override fun getAttachment(message: Message, attachmentId: Long, attachmentName: String) {
MobidziennikLoginWeb(data) {
login(LOGIN_METHOD_MOBIDZIENNIK_WEB) {
MobidziennikWebGetAttachment(data, message, attachmentId, attachmentName) {
completed()
}
}
}
override fun firstLogin() {
MobidziennikFirstLogin(data) {
completed()
override fun getRecipientList() {
login(LOGIN_METHOD_MOBIDZIENNIK_WEB) {
MobidziennikWebGetRecipientList(data) {
completed()
}
}
}
override fun firstLogin() { MobidziennikFirstLogin(data) { completed() } }
override fun cancel() {
d(TAG, "Cancelled")
data.cancel()
@ -106,28 +131,25 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() {
callback.onCompleted()
}
override fun onProgress(step: Float) {
callback.onProgress(step)
}
override fun onStartProgress(stringRes: Int) {
callback.onStartProgress(stringRes)
}
override fun onCompleted() { callback.onCompleted() }
override fun onProgress(step: Float) { callback.onProgress(step) }
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
override fun onError(apiError: ApiError) {
if (apiError.errorCode in internalErrorList) {
// finish immediately if the same error occurs twice during the same sync
callback.onError(apiError)
return
}
internalErrorList.add(apiError.errorCode)
when (apiError.errorCode) {
in internalErrorList -> {
// finish immediately if the same error occurs twice during the same sync
callback.onError(apiError)
}
CODE_INTERNAL_LIBRUS_ACCOUNT_410 -> {
internalErrorList.add(apiError.errorCode)
loginStore.removeLoginData("refreshToken") // force a clean login
//loginLibrus()
ERROR_MOBIDZIENNIK_WEB_ACCESS_DENIED,
ERROR_MOBIDZIENNIK_WEB_NO_SESSION_KEY,
ERROR_MOBIDZIENNIK_WEB_NO_SESSION_VALUE,
ERROR_MOBIDZIENNIK_WEB_NO_SERVER_ID -> {
data.loginMethods.remove(LOGIN_METHOD_MOBIDZIENNIK_WEB)
data.prepareFor(mobidziennikLoginMethods, LOGIN_METHOD_MOBIDZIENNIK_WEB)
data.webSessionIdExpiryTime = 0
login()
}
else -> callback.onError(apiError)
}

View File

@ -26,8 +26,15 @@ open class MobidziennikWeb(open val data: DataMobidziennik) {
val profile
get() = data.profile
fun webGet(tag: String, endpoint: String, method: Int = GET, onSuccess: (text: String) -> Unit) {
val url = "https://${data.loginServerName}.mobidziennik.pl$endpoint"
fun webGet(
tag: String,
endpoint: String,
method: Int = GET,
parameters: List<Pair<String, Any>> = emptyList(),
fullUrl: String? = null,
onSuccess: (text: String) -> Unit
) {
val url = fullUrl ?: "https://${data.loginServerName}.mobidziennik.pl$endpoint"
d(tag, "Request: Mobidziennik/Web - $url")
@ -91,6 +98,15 @@ open class MobidziennikWeb(open val data: DataMobidziennik) {
Request.builder()
.url(url)
.userAgent(MOBIDZIENNIK_USER_AGENT)
.apply {
when (method) {
GET -> get()
POST -> post()
}
parameters.map { (name, value) ->
addParameter(name, value)
}
}
.callback(callback)
.build()
.enqueue()

View File

@ -15,6 +15,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipientFull
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.fixName
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.singleOrNull
import pl.szczodrzynski.edziennik.utils.Utils.monthFromName
@ -67,7 +68,7 @@ class MobidziennikWebGetMessage(
message.id
)
recipient.fullName = profile?.accountNameLong ?: profile?.studentNameLong
recipient.fullName = profile?.accountNameLong ?: profile?.studentNameLong ?: ""
messageRecipientList.add(recipient)
} else {
@ -76,7 +77,7 @@ class MobidziennikWebGetMessage(
content.select("table.spis tr:has(td)")?.forEach { recipientEl ->
val senderEl = recipientEl.select("td:eq(0)").first()
val senderName = senderEl.text()
val senderName = senderEl.text().fixName()
val teacher = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName }
val receiverId = teacher?.id ?: -1
@ -117,7 +118,7 @@ class MobidziennikWebGetMessage(
// this needs to be at the end
message.apply {
this.body = body.html()
this.body = body.html().replace("\n", "<br>")
clearAttachments()
content.select("ul li").map { it.select("a").first() }.forEach {

View File

@ -0,0 +1,98 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-22.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web
import androidx.core.util.set
import androidx.room.OnConflictStrategy
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.Regexes
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb
import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
class MobidziennikWebGetRecipientList(
override val data: DataMobidziennik, val onSuccess: () -> Unit) : MobidziennikWeb(data) {
companion object {
private const val TAG = "MobidziennikWebGetRecipientList"
}
init {
webGet(TAG, "/mobile/dodajwiadomosc") { text ->
Regexes.MOBIDZIENNIK_MESSAGE_RECIPIENTS_JSON.find(text)?.let { match ->
val recipientLists = JsonParser().parse(match[1]).asJsonArray
recipientLists?.asJsonObjectList()?.forEach { list ->
val listType = list.getString("typ")?.toIntOrNull() ?: -1
val listName = list.getString("nazwa") ?: ""
list.getJsonArray("dane")?.asJsonObjectList()?.forEach { recipient ->
if (recipient.getBoolean("lista") == true) {
recipient.getJsonArray("dane")?.asJsonObjectList()?.forEach {
processRecipient(listType, recipient.getString("nazwa") ?: "", it)
}
}
else
processRecipient(listType, listName, recipient)
}
}
}
val event = RecipientListGetEvent(
data.profileId,
data.teacherList.filter { it.loginId != null }
)
profile?.lastReceiversSync = System.currentTimeMillis()
data.teacherOnConflictStrategy = OnConflictStrategy.REPLACE
EventBus.getDefault().postSticky(event)
onSuccess()
}
}
private fun processRecipient(listType: Int, listName: String, recipient: JsonObject) {
val id = recipient.getLong("id") ?: -1
// get teacher by ID or create it
val teacher = data.teacherList[id] ?: Teacher(data.profileId, id).apply {
val fullName = recipient.getString("nazwa")?.fixName()
name = fullName ?: ""
fullName?.splitName()?.let {
name = it.second
surname = it.first
}
data.teacherList[id] = this
}
teacher.apply {
loginId = id.toString()
when (listType) {
1 -> setTeacherType(Teacher.TYPE_PRINCIPAL)
2 -> setTeacherType(Teacher.TYPE_TEACHER)
3 -> setTeacherType(Teacher.TYPE_PARENT)
4 -> setTeacherType(Teacher.TYPE_STUDENT)
//5 -> Użytkownicy zewnętrzni
//6 -> Samorządy klasowe
7 -> setTeacherType(Teacher.TYPE_PARENTS_COUNCIL) // Rady oddziałowe rodziców
8 -> {
setTeacherType(Teacher.TYPE_EDUCATOR)
typeDescription = listName
}
9 -> setTeacherType(Teacher.TYPE_PEDAGOGUE)
10 -> setTeacherType(Teacher.TYPE_SPECIALIST)
else -> when (listName) {
"Administratorzy" -> setTeacherType(Teacher.TYPE_SCHOOL_ADMIN)
"Sekretarka" -> setTeacherType(Teacher.TYPE_SECRETARIAT)
"Wsparcie techniczne mobiDziennik" -> setTeacherType(Teacher.TYPE_SUPER_ADMIN)
else -> {
setTeacherType(Teacher.TYPE_OTHER)
typeDescription = listName
}
}
}
}
}
}

View File

@ -14,6 +14,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.fixName
import pl.szczodrzynski.edziennik.singleOrNull
import pl.szczodrzynski.edziennik.utils.models.Date
@ -55,14 +56,15 @@ class MobidziennikWebMessagesAll(override val data: DataMobidziennik,
if (type == TYPE_RECEIVED) {
// search sender teacher
val senderName = senderEl.text()
val senderName = senderEl.text().fixName()
senderId = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName }?.id ?: -1
data.messageRecipientList.add(MessageRecipient(profileId, -1, id))
} else {
// TYPE_SENT, so multiple recipients possible
val recipientNames = senderEl.text().split(", ")
for (recipientName in recipientNames) {
val recipientId = data.teacherList.singleOrNull { it.fullNameLastFirst == recipientName }?.id ?: -1
val name = recipientName.fixName()
val recipientId = data.teacherList.singleOrNull { it.fullNameLastFirst == name }?.id ?: -1
data.messageRecipientIgnoreList.add(MessageRecipient(profileId, recipientId, id))
}
}

View File

@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.fixName
import pl.szczodrzynski.edziennik.singleOrNull
import pl.szczodrzynski.edziennik.utils.models.Date
@ -48,7 +49,7 @@ class MobidziennikWebMessagesInbox(override val data: DataMobidziennik,
val addedDate = Date.fromIsoHm(addedDateEl.text())
val senderEl = item.select("td:eq(2)").first()
val senderName = senderEl.ownText()
val senderName = senderEl.ownText().fixName()
val senderId = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName }?.id ?: -1
data.messageRecipientIgnoreList.add(MessageRecipient(profileId, -1, id))

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-26.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.data.api.POST
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb
import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
class MobidziennikWebSendMessage(
override val data: DataMobidziennik,
val recipients: List<Teacher>,
val subject: String,
val text: String,
val onSuccess: () -> Unit
) : MobidziennikWeb(data) {
companion object {
private const val TAG = "MobidziennikWebSendMessage"
}
init {
val params = mutableListOf<Pair<String, Any>>(
"nazwa" to subject,
"tresc" to text
)
for (teacher in recipients) {
teacher.loginId?.let {
params += "odbiorcy[]" to it
}
}
webGet(TAG, endpoint = "/dziennik/dodajwiadomosc", method = POST, parameters = params) { text ->
if (!text.contains(">Wiadomość została wysłana.<")) {
// TODO error
return@webGet
}
// TODO create MobidziennikWebMessagesSent and replace this
MobidziennikWebMessagesAll(data) {
val message = data.messageIgnoreList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject }
val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id }
val event = MessageSentEvent(data.profileId, message, metadata?.addedDate)
EventBus.getDefault().postSticky(event)
onSuccess()
}
}
}
}

View File

@ -20,6 +20,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.utils.Utils.d
class Template(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
@ -68,6 +69,10 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore,
}
override fun sendMessage(recipients: List<Teacher>, subject: String, text: String) {
}
override fun markAllAnnouncementsAsRead() {
}
@ -80,6 +85,10 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore,
}
override fun getRecipientList() {
}
override fun firstLogin() {
TemplateFirstLogin(data) {
completed()

View File

@ -171,7 +171,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
"P02" -> "http://efeb-komunikacja.pro-hudsonrc.win.vulcan.pl"
"P90" -> "http://efeb-komunikacja-pro-mwujakowska.neo.win.vulcan.pl"
"FK1", "FS1" -> "http://api.fakelog.cf"
"SZ9" -> "http://vulcan.szkolny.eu"
"SZ9" -> "http://hack.szkolny.eu"
else -> null
}
return if (url != null) "$url/$symbol" else loginStore.getLoginData("apiUrl", null)

View File

@ -6,22 +6,24 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.CODE_INTERNAL_LIBRUS_ACCOUNT_410
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanData
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiMessagesChangeStatus
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiSendMessage
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.firstlogin.VulcanFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginApi
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.prepare
import pl.szczodrzynski.edziennik.data.api.prepareFor
import pl.szczodrzynski.edziennik.data.api.vulcanLoginMethods
import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.utils.Utils.d
class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
@ -31,6 +33,7 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
val internalErrorList = mutableListOf<Int>()
val data: DataVulcan
private var afterLogin: (() -> Unit)? = null
init {
data = DataVulcan(app, profile, loginStore).apply {
@ -57,18 +60,44 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
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}")
login()
}
private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) {
d(TAG, "Trying to login with ${data.targetLoginMethodIds}")
if (internalErrorList.isNotEmpty()) {
d(TAG, " - Internal errors:")
internalErrorList.forEach { d(TAG, " - code $it") }
}
loginMethodId?.let { data.prepareFor(vulcanLoginMethods, it) }
afterLogin?.let { this.afterLogin = it }
VulcanLogin(data) {
VulcanData(data) {
data()
}
}
private fun data() {
d(TAG, "Endpoint IDs: ${data.targetEndpointIds}")
if (internalErrorList.isNotEmpty()) {
d(TAG, " - Internal errors:")
internalErrorList.forEach { d(TAG, " - code $it") }
}
afterLogin?.invoke() ?: VulcanData(data) {
completed()
}
}
override fun getMessage(message: MessageFull) {
login(LOGIN_METHOD_VULCAN_API) {
VulcanApiMessagesChangeStatus(data, message) {
completed()
}
}
}
override fun getMessage(message: MessageFull) {
VulcanLoginApi(data) {
VulcanApiMessagesChangeStatus(data, message) {
override fun sendMessage(recipients: List<Teacher>, subject: String, text: String) {
login(LOGIN_METHOD_VULCAN_API) {
VulcanApiSendMessage(data, recipients, subject, text) {
completed()
}
}
@ -86,12 +115,11 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
override fun firstLogin() {
VulcanFirstLogin(data) {
completed()
}
override fun getRecipientList() {
}
override fun firstLogin() { VulcanFirstLogin(data) { completed() } }
override fun cancel() {
d(TAG, "Cancelled")
data.cancel()
@ -99,29 +127,17 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() {
callback.onCompleted()
}
override fun onProgress(step: Float) {
callback.onProgress(step)
}
override fun onStartProgress(stringRes: Int) {
callback.onStartProgress(stringRes)
}
override fun onCompleted() { callback.onCompleted() }
override fun onProgress(step: Float) { callback.onProgress(step) }
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
override fun onError(apiError: ApiError) {
if (apiError.errorCode in internalErrorList) {
// finish immediately if the same error occurs twice during the same sync
callback.onError(apiError)
return
}
internalErrorList.add(apiError.errorCode)
when (apiError.errorCode) {
in internalErrorList -> {
// finish immediately if the same error occurs twice during the same sync
callback.onError(apiError)
}
CODE_INTERNAL_LIBRUS_ACCOUNT_410 -> {
internalErrorList.add(apiError.errorCode)
loginStore.removeLoginData("refreshToken") // force a clean login
//loginLibrus()
}
else -> callback.onError(apiError)
}
}

View File

@ -27,6 +27,9 @@ open class VulcanApi(open val data: DataVulcan) {
val profileId
get() = data.profile?.id ?: -1
val profile
get() = data.profile
fun apiGet(
tag: String,
endpoint: String,

View File

@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.models.Date
import kotlin.text.replace
class VulcanApiMessagesInbox(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) {
companion object {
@ -26,11 +27,11 @@ class VulcanApiMessagesInbox(override val data: DataVulcan, val onSuccess: () ->
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 startDate = when (profile.empty) {
true -> profile.getSemesterStart(profile.currentSemester).inUnix
else -> Date.getToday().stepForward(0, -2, 0).inUnix
}
val endDate: String = profile.getSemesterEnd(profile.currentSemester).stringY_m_d
val endDate = Date.getToday().stepForward(0, 1, 0).inUnix
apiGet(TAG, VULCAN_API_ENDPOINT_MESSAGES_RECEIVED, parameters = mapOf(
"DataPoczatkowa" to startDate,
@ -71,7 +72,7 @@ class VulcanApiMessagesInbox(override val data: DataVulcan, val onSuccess: () ->
profileId,
id,
subject,
body,
body.replace("\n", "<br>"),
TYPE_RECEIVED,
senderId,
-1

View File

@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.models.Date
import kotlin.text.replace
class VulcanApiMessagesSent(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) {
companion object {
@ -25,11 +26,12 @@ class VulcanApiMessagesSent(override val data: DataVulcan, val onSuccess: () ->
init {
data.profile?.also { profile ->
val startDate: Long = when (profile.empty) {
val startDate = when (profile.empty) {
true -> profile.getSemesterStart(profile.currentSemester).inUnix
else -> Date.getToday().stepForward(0, -1, 0).inUnix
else -> Date.getToday().stepForward(0, -2, 0).inUnix
}
val endDate: Long = profile.getSemesterEnd(profile.currentSemester).inUnix
val endDate = Date.getToday().stepForward(0, 1, 0).inUnix
apiGet(TAG, VULCAN_API_ENDPOINT_MESSAGES_SENT, parameters = mapOf(
"DataPoczatkowa" to startDate,
@ -90,7 +92,7 @@ class VulcanApiMessagesSent(override val data: DataVulcan, val onSuccess: () ->
profileId,
id,
subject,
body,
body.replace("\n", "<br>"),
TYPE_SENT,
-1,
-1

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-29.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_MESSAGES_ADD
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
class VulcanApiSendMessage(
override val data: DataVulcan,
val recipients: List<Teacher>,
val subject: String,
val text: String,
val onSuccess: () -> Unit
) : VulcanApi(data) {
companion object {
private const val TAG = "VulcanApiSendMessage"
}
init {
val recipientsArray = JsonArray()
for (teacher in recipients) {
teacher.loginId?.let {
recipientsArray += JsonObject(
"LoginId" to it,
"Nazwa" to "${teacher.fullNameLastFirst} - pracownik"
)
}
}
val params = mapOf(
"NadawcaWiadomosci" to (profile?.accountNameLong ?: profile?.studentNameLong ?: ""),
"Tytul" to subject,
"Tresc" to text,
"Adresaci" to recipientsArray,
"LoginId" to data.studentLoginId,
"IdUczen" to data.studentId
)
apiGet(TAG, VULCAN_API_ENDPOINT_MESSAGES_ADD, parameters = params) { json, _ ->
val messageId = json.getJsonObject("Data").getLong("WiadomoscId")
if (messageId == null) {
// TODO error
return@apiGet
}
VulcanApiMessagesSent(data) {
val message = data.messageIgnoreList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject }
val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == messageId }
val event = MessageSentEvent(data.profileId, message, metadata?.addedDate)
EventBus.getDefault().postSticky(event)
onSuccess()
}
}
}
}

View File

@ -0,0 +1,9 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-27.
*/
package pl.szczodrzynski.edziennik.data.api.events
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
data class MessageSentEvent(val profileId: Int, val message: Message?, val sentDate: Long?)

View File

@ -0,0 +1,9 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-22.
*/
package pl.szczodrzynski.edziennik.data.api.events
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
data class RecipientListGetEvent(val profileId: Int, val teacherList: List<Teacher>)

View File

@ -8,13 +8,16 @@ import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
interface EdziennikInterface {
fun sync(featureIds: List<Int>, viewId: Int? = null, arguments: JsonObject? = null)
fun getMessage(message: MessageFull)
fun sendMessage(recipients: List<Teacher>, subject: String, text: String)
fun markAllAnnouncementsAsRead()
fun getAnnouncement(announcement: AnnouncementFull)
fun getAttachment(message: Message, attachmentId: Long, attachmentName: String)
fun getRecipientList()
fun firstLogin()
fun cancel()
}

View File

@ -8,10 +8,11 @@ import android.content.Context
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.request.ErrorReportRequest
import pl.szczodrzynski.edziennik.stackTraceString
class ApiError(val tag: String, val errorCode: Int) {
class ApiError(val tag: String, var errorCode: Int) {
val id = System.currentTimeMillis()
var profileId: Int? = null
var throwable: Throwable? = null
@ -61,7 +62,7 @@ class ApiError(val tag: String, val errorCode: Int) {
if (it != 0)
context.getString(it)
else
"?"
context.getString(R.string.error_unknown_format, errorCode, tag)
}
}

View File

@ -1,324 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-11-26
*/
package pl.szczodrzynski.edziennik.data.api.models;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;
import com.google.gson.JsonObject;
import java.io.InterruptedIOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import javax.net.ssl.SSLException;
import im.wangchao.mhttp.Request;
import im.wangchao.mhttp.Response;
import pl.szczodrzynski.edziennik.R;
public class AppError {
public static final int CODE_OTHER = 0;
public static final int CODE_OK = 1;
public static final int CODE_NO_INTERNET = 10;
public static final int CODE_SSL_ERROR = 13;
public static final int CODE_ARCHIVED = 5;
public static final int CODE_MAINTENANCE = 6;
public static final int CODE_LOGIN_ERROR = 7;
public static final int CODE_ACCOUNT_MISMATCH = 8;
public static final int CODE_APP_SERVER_ERROR = 9;
public static final int CODE_MULTIACCOUNT_SETUP = 12;
public static final int CODE_TIMEOUT = 11;
public static final int CODE_PROFILE_NOT_FOUND = 14;
public static final int CODE_ATTACHMENT_NOT_AVAILABLE = 28;
// user's fault
public static final int CODE_INVALID_LOGIN = 2;
public static final int CODE_INVALID_SERVER_ADDRESS = 21;
public static final int CODE_INVALID_SCHOOL_NAME = 22;
public static final int CODE_INVALID_DEVICE = 23;
public static final int CODE_OLD_PASSWORD = 4;
public static final int CODE_INVALID_TOKEN = 24;
public static final int CODE_EXPIRED_TOKEN = 27;
public static final int CODE_INVALID_SYMBOL = 25;
public static final int CODE_INVALID_PIN = 26;
public static final int CODE_LIBRUS_NOT_ACTIVATED = 29;
public static final int CODE_SYNERGIA_NOT_ACTIVATED = 32;
public static final int CODE_LIBRUS_DISCONNECTED = 31;
public static final int CODE_PROFILE_ARCHIVED = 30;
public static final int CODE_INTERNAL_MISSING_DATA = 100;
// internal errors - not for user's information.
// these error codes are processed in API main classes
public String TAG;
public int line;
public int errorCode;
public String errorText;
public Response response;
public Request request;
public Throwable throwable;
public String apiResponse;
public AppError(String TAG, int line, int errorCode, String errorText, Response response, Request request, Throwable throwable, String apiResponse) {
this.TAG = TAG;
this.line = line;
this.errorCode = errorCode;
this.errorText = errorText;
this.response = response;
this.request = request;
this.throwable = throwable;
this.apiResponse = apiResponse;
}
public AppError(String TAG, int line, int errorCode) {
this(TAG, line, errorCode, null, null, null, null, null);
}
public AppError(String TAG, int line, int errorCode, Response response, Throwable throwable) {
this(TAG, line, errorCode, null, response, response == null ? null : response.request(), throwable, null);
}
public AppError(String TAG, int line, int errorCode, Response response) {
this(TAG, line, errorCode, null, response, response == null ? null : response.request(), null, null);
}
public AppError(String TAG, int line, int errorCode, Throwable throwable, String apiResponse) {
this(TAG, line, errorCode, null, null, null, throwable, apiResponse);
}
public AppError(String TAG, int line, int errorCode, Throwable throwable, JsonObject apiResponse) {
this(TAG, line, errorCode, null, null, null, throwable, apiResponse == null ? null : apiResponse.toString());
}
public AppError(String TAG, int line, int errorCode, String errorText, Response response, JsonObject apiResponse) {
this(TAG, line, errorCode, errorText, response, response == null ? null : response.request(), null, apiResponse == null ? null : apiResponse.toString());
}
public AppError(String TAG, int line, int errorCode, String errorText, Response response, String apiResponse) {
this(TAG, line, errorCode, errorText, response, response == null ? null : response.request(), null, apiResponse);
}
public AppError(String TAG, int line, int errorCode, String errorText, String apiResponse) {
this(TAG, line, errorCode, errorText, null, null, null, apiResponse);
}
public AppError(String TAG, int line, int errorCode, String errorText, JsonObject apiResponse) {
this(TAG, line, errorCode, errorText, null, null, null, apiResponse == null ? null : apiResponse.toString());
}
public AppError(String TAG, int line, int errorCode, String errorText) {
this(TAG, line, errorCode, errorText, null, null, null, null);
}
public AppError(String TAG, int line, int errorCode, JsonObject apiResponse) {
this(TAG, line, errorCode, null, null, null, null, apiResponse.toString());
}
public AppError(String TAG, int line, int errorCode, Response response, Throwable throwable, JsonObject apiResponse) {
this(TAG, line, errorCode, null, response, response == null ? null : response.request(), throwable, apiResponse == null ? null : apiResponse.toString());
}
public AppError(String TAG, int line, int errorCode, Response response, Throwable throwable, String apiResponse) {
this(TAG, line, errorCode, null, response, response == null ? null : response.request(), throwable, apiResponse);
}
public AppError(String TAG, int line, int errorCode, Response response, String apiResponse) {
this(TAG, line, errorCode, null, response, response == null ? null : response.request(), null, apiResponse);
}
public AppError(String TAG, int line, int errorCode, Response response, JsonObject apiResponse) {
this(TAG, line, errorCode, null, response, response == null ? null : response.request(), null, apiResponse == null ? null : apiResponse.toString());
}
public String getDetails(Context context) {
StringBuilder sb = new StringBuilder();
sb.append(stringErrorCode(context, errorCode, errorText)).append("\n");
sb.append("(").append(stringErrorType(errorCode)).append("#").append(errorCode).append(")\n");
sb.append("at ").append(TAG).append(":").append(line).append("\n");
sb.append("\n");
if (throwable == null)
sb.append("Throwable is null");
else
sb.append(Log.getStackTraceString(throwable));
sb.append("\n");
sb.append(Build.MANUFACTURER).append(" ").append(Build.BRAND).append(" ").append(Build.MODEL).append(" ").append(Build.DEVICE).append("\n");
return sb.toString();
}
public interface GetApiResponseCallback {
void onSuccess(String apiResponse);
}
/**
*
* @param context a Context
* @param apiResponseCallback a callback executed on a worker thread
*/
public void getApiResponse(Context context, GetApiResponseCallback apiResponseCallback) {
StringBuilder sb = new StringBuilder();
sb.append("Request:\n");
if (request != null) {
sb.append(request.method()).append(" ").append(request.url().toString()).append("\n");
sb.append(request.headers().toString()).append("\n");
sb.append("\n");
sb.append(request.bodyToString()).append("\n\n");
}
else
sb.append("null\n\n");
if (apiResponse == null && response != null)
apiResponse = response.parserErrorBody;
sb.append("Response:\n");
if (response != null) {
sb.append(response.code()).append(" ").append(response.message()).append("\n");
sb.append(response.headers().toString()).append("\n");
sb.append("\n");
if (apiResponse == null) {
if (Thread.currentThread().getName().equals("main")) {
AsyncTask.execute(() -> {
if (response.raw().body() != null) {
try {
sb.append(response.raw().body().string());
} catch (Exception e) {
sb.append("Exception while getting response body:\n").append(Log.getStackTraceString(e));
}
}
else {
sb.append("null");
}
apiResponseCallback.onSuccess(sb.toString());
});
}
else {
if (response.raw().body() != null) {
try {
sb.append(response.raw().body().string());
} catch (Exception e) {
sb.append("Exception while getting response body:\n").append(Log.getStackTraceString(e));
}
}
else {
sb.append("null");
}
apiResponseCallback.onSuccess(sb.toString());
}
return;
}
}
else
sb.append("null\n\n");
sb.append("API Response:\n");
if (apiResponse != null) {
sb.append(apiResponse).append("\n\n");
}
else {
sb.append("null\n\n");
}
apiResponseCallback.onSuccess(sb.toString());
}
public AppError changeIfCodeOther() {
if (errorCode != CODE_OTHER && errorCode != CODE_MAINTENANCE)
return this;
if (throwable instanceof UnknownHostException)
errorCode = CODE_NO_INTERNET;
else if (throwable instanceof SSLException)
errorCode = CODE_SSL_ERROR;
else if (throwable instanceof SocketTimeoutException)
errorCode = CODE_TIMEOUT;
else if (throwable instanceof InterruptedIOException)
errorCode = CODE_NO_INTERNET;
else if (response != null &&
(response.code() == 424
|| response.code() == 400
|| response.code() == 401
|| response.code() == 500
|| response.code() == 503
|| response.code() == 404))
errorCode = CODE_MAINTENANCE;
return this;
}
public String asReadableString(Context context) {
return stringErrorCode(context, errorCode, errorText) + (errorCode == CODE_MAINTENANCE && errorText != null && !errorText.isEmpty() ? " ("+errorText+")" : "");
}
public static String stringErrorCode(Context context, int errorCode, String errorText)
{
switch (errorCode) {
case CODE_OK:
return context.getString(R.string.sync_error_ok);
case CODE_INVALID_LOGIN:
return context.getString(R.string.sync_error_invalid_login);
case CODE_LOGIN_ERROR:
return context.getString(R.string.sync_error_login_error);
case CODE_INVALID_DEVICE:
return context.getString(R.string.sync_error_invalid_device);
case CODE_OLD_PASSWORD:
return context.getString(R.string.sync_error_old_password);
case CODE_ARCHIVED:
return context.getString(R.string.sync_error_archived);
case CODE_MAINTENANCE:
return context.getString(R.string.sync_error_maintenance);
case CODE_NO_INTERNET:
return context.getString(R.string.sync_error_no_internet);
case CODE_ACCOUNT_MISMATCH:
return context.getString(R.string.sync_error_account_mismatch);
case CODE_APP_SERVER_ERROR:
return context.getString(R.string.sync_error_app_server);
case CODE_TIMEOUT:
return context.getString(R.string.sync_error_timeout);
case CODE_SSL_ERROR:
return context.getString(R.string.sync_error_ssl);
case CODE_INVALID_SERVER_ADDRESS:
return context.getString(R.string.sync_error_invalid_server_address);
case CODE_INVALID_SCHOOL_NAME:
return context.getString(R.string.sync_error_invalid_school_name);
case CODE_PROFILE_NOT_FOUND:
return context.getString(R.string.sync_error_profile_not_found);
case CODE_INVALID_TOKEN:
return context.getString(R.string.sync_error_invalid_token);
case CODE_ATTACHMENT_NOT_AVAILABLE:
return context.getString(R.string.sync_error_attachment_not_available);
case CODE_LIBRUS_NOT_ACTIVATED:
return context.getString(R.string.sync_error_librus_not_activated);
case CODE_PROFILE_ARCHIVED:
return context.getString(R.string.sync_error_profile_archived);
case CODE_LIBRUS_DISCONNECTED:
return context.getString(R.string.sync_error_librus_disconnected);
case CODE_SYNERGIA_NOT_ACTIVATED:
return context.getString(R.string.sync_error_synergia_not_activated);
default:
case CODE_MULTIACCOUNT_SETUP:
case CODE_OTHER:
return errorText != null ? errorText : context.getString(R.string.sync_error_unknown);
}
}
public static String stringErrorType(int errorCode)
{
switch (errorCode) {
default:
case CODE_OTHER: return "CODE_OTHER";
case CODE_OK: return "CODE_OK";
case CODE_NO_INTERNET: return "CODE_NO_INTERNET";
case CODE_SSL_ERROR: return "CODE_SSL_ERROR";
case CODE_ARCHIVED: return "CODE_ARCHIVED";
case CODE_MAINTENANCE: return "CODE_MAINTENANCE";
case CODE_LOGIN_ERROR: return "CODE_LOGIN_ERROR";
case CODE_ACCOUNT_MISMATCH: return "CODE_ACCOUNT_MISMATCH";
case CODE_APP_SERVER_ERROR: return "CODE_APP_SERVER_ERROR";
case CODE_MULTIACCOUNT_SETUP: return "CODE_MULTIACCOUNT_SETUP";
case CODE_TIMEOUT: return "CODE_TIMEOUT";
case CODE_PROFILE_NOT_FOUND: return "CODE_PROFILE_NOT_FOUND";
case CODE_INVALID_LOGIN: return "CODE_INVALID_LOGIN";
case CODE_INVALID_SERVER_ADDRESS: return "CODE_INVALID_SERVER_ADDRESS";
case CODE_INVALID_SCHOOL_NAME: return "CODE_INVALID_SCHOOL_NAME";
case CODE_INVALID_DEVICE: return "CODE_INVALID_DEVICE";
case CODE_OLD_PASSWORD: return "CODE_OLD_PASSWORD";
case CODE_INVALID_TOKEN: return "CODE_INVALID_TOKEN";
case CODE_EXPIRED_TOKEN: return "CODE_EXPIRED_TOKEN";
case CODE_INVALID_SYMBOL: return "CODE_INVALID_SYMBOL";
case CODE_INVALID_PIN: return "CODE_INVALID_PIN";
case CODE_ATTACHMENT_NOT_AVAILABLE: return "CODE_ATTACHMENT_NOT_AVAILABLE";
case CODE_LIBRUS_NOT_ACTIVATED: return "CODE_LIBRUS_NOT_ACTIVATED";
case CODE_PROFILE_ARCHIVED: return "CODE_PROFILE_ARCHIVED";
case CODE_LIBRUS_DISCONNECTED: return "CODE_LIBRUS_DISCONNECTED";
case CODE_SYNERGIA_NOT_ACTIVATED: return "CODE_SYNERGIA_NOT_ACTIVATED";
}
}
}

View File

@ -3,13 +3,12 @@ package pl.szczodrzynski.edziennik.data.api.models
import android.util.LongSparseArray
import android.util.SparseArray
import androidx.core.util.size
import androidx.room.OnConflictStrategy
import com.google.gson.JsonObject
import im.wangchao.mhttp.Response
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.DataNotifications
import pl.szczodrzynski.edziennik.data.api.EXCEPTION_NOTIFY
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.interfaces.EndpointCallback
import pl.szczodrzynski.edziennik.data.api.models.AppError.*
import pl.szczodrzynski.edziennik.data.db.AppDb
import pl.szczodrzynski.edziennik.data.db.modules.announcements.Announcement
import pl.szczodrzynski.edziennik.data.db.modules.api.EndpointTimer
@ -117,6 +116,8 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
val lessonRanges = SparseArray<LessonRange>()
val gradeCategories = LongSparseArray<GradeCategory>()
var teacherOnConflictStrategy = OnConflictStrategy.IGNORE
val classrooms = LongSparseArray<Classroom>()
val attendanceTypes = LongSparseArray<AttendanceType>()
val noticeTypes = LongSparseArray<NoticeType>()
@ -257,9 +258,10 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
// always present and not empty, during every sync
db.endpointTimerDao().addAll(endpointTimers)
db.teacherDao().clear(profileId)
db.teacherDao().addAll(teacherList.values())
db.subjectDao().clear(profileId)
if (teacherOnConflictStrategy == OnConflictStrategy.IGNORE)
db.teacherDao().addAllIgnore(teacherList.values())
else if (teacherOnConflictStrategy == OnConflictStrategy.REPLACE)
db.teacherDao().addAll(teacherList.values())
db.subjectDao().addAll(subjectList.values())
db.teamDao().clear(profileId)
db.teamDao().addAll(teamList.values())
@ -380,7 +382,7 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
return (db.luckyNumberDao().getNearestFutureNow(profileId, Date.getToday().value) ?: -1) == -1
}
fun error(tag: String, errorCode: Int, response: Response? = null, throwable: Throwable? = null, apiResponse: JsonObject? = null) {
/*fun error(tag: String, errorCode: Int, response: Response? = null, throwable: Throwable? = null, apiResponse: JsonObject? = null) {
val code = when (throwable) {
is UnknownHostException, is SSLException, is InterruptedIOException -> CODE_NO_INTERNET
is SocketTimeoutException -> CODE_TIMEOUT
@ -392,23 +394,36 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
error(ApiError(tag, code).apply {
profileId = profile?.id ?: -1
}.withResponse(response).withThrowable(throwable).withApiResponse(apiResponse))
}
}*/
fun error(tag: String, errorCode: Int, response: Response? = null, apiResponse: String? = null) {
val code = when (null) {
is UnknownHostException, is SSLException, is InterruptedIOException -> CODE_NO_INTERNET
is SocketTimeoutException -> CODE_TIMEOUT
else -> when (response?.code()) {
400, 401, 424, 500, 503, 404 -> CODE_MAINTENANCE
else -> errorCode
}
}
error(ApiError(tag, code).apply {
error(ApiError(tag, errorCode).apply {
profileId = profile?.id ?: -1
}.withResponse(response).withApiResponse(apiResponse))
}
fun error(apiError: ApiError) {
apiError.errorCode = when (apiError.throwable) {
is UnknownHostException -> ERROR_REQUEST_FAILURE_HOSTNAME_NOT_FOUND
is SSLException -> ERROR_REQUEST_FAILURE_SSL_ERROR
is InterruptedIOException -> ERROR_REQUEST_FAILURE_NO_INTERNET
is SocketTimeoutException -> ERROR_REQUEST_FAILURE_TIMEOUT
else ->
if (apiError.errorCode == ERROR_REQUEST_FAILURE)
when (apiError.response?.code()) {
400 -> ERROR_REQUEST_HTTP_400
401 -> ERROR_REQUEST_HTTP_401
403 -> ERROR_REQUEST_HTTP_403
404 -> ERROR_REQUEST_HTTP_404
405 -> ERROR_REQUEST_HTTP_405
410 -> ERROR_REQUEST_HTTP_410
424 -> ERROR_REQUEST_HTTP_424
500 -> ERROR_REQUEST_HTTP_500
503 -> ERROR_REQUEST_HTTP_503
else -> apiError.errorCode
}
else apiError.errorCode
}
callback.onError(apiError)
}

View File

@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.announcements.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTask(profileId) {
companion object {
@ -26,9 +27,11 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
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, message: MessageFull) = EdziennikTask(profileId, MessageGetRequest(message))
fun messageSend(profileId: Int, recipients: List<Teacher>, subject: String, text: String) = EdziennikTask(profileId, MessageSendRequest(recipients, subject, text))
fun announcementsRead(profileId: Int) = EdziennikTask(profileId, AnnouncementsReadRequest())
fun announcementGet(profileId: Int, announcement: AnnouncementFull) = EdziennikTask(profileId, AnnouncementGetRequest(announcement))
fun attachmentGet(profileId: Int, message: Message, attachmentId: Long, attachmentName: String) = EdziennikTask(profileId, AttachmentGetRequest(message, attachmentId, attachmentName))
fun recipientListGet(profileId: Int) = EdziennikTask(profileId, RecipientListGetRequest())
}
private lateinit var loginStore: LoginStore
@ -77,10 +80,12 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
viewId = request.viewIds?.get(0)?.first,
arguments = request.arguments)
is MessageGetRequest -> edziennikInterface?.getMessage(request.message)
is MessageSendRequest -> edziennikInterface?.sendMessage(request.recipients, request.subject, request.text)
is FirstLoginRequest -> edziennikInterface?.firstLogin()
is AnnouncementsReadRequest -> edziennikInterface?.markAllAnnouncementsAsRead()
is AnnouncementGetRequest -> edziennikInterface?.getAnnouncement(request.announcement)
is AttachmentGetRequest -> edziennikInterface?.getAttachment(request.message, request.attachmentId, request.attachmentName)
is RecipientListGetRequest -> edziennikInterface?.getRecipientList()
}
}
@ -97,7 +102,9 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
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 message: MessageFull)
data class MessageSendRequest(val recipients: List<Teacher>, val subject: String, val text: String)
class AnnouncementsReadRequest
data class AnnouncementGetRequest(val announcement: AnnouncementFull)
data class AttachmentGetRequest(val message: Message, val attachmentId: Long, val attachmentName: String)
class RecipientListGetRequest
}

View File

@ -108,7 +108,7 @@ import pl.szczodrzynski.edziennik.utils.models.Date;
AttendanceType.class,
pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson.class,
ConfigEntry.class,
Metadata.class}, version = 70)
Metadata.class}, version = 71)
@TypeConverters({
ConverterTime.class,
ConverterDate.class,
@ -836,6 +836,19 @@ public abstract class AppDb extends RoomDatabase {
database.execSQL("DELETE FROM announcements");
}
};
private static final Migration MIGRATION_70_71 = new Migration(70, 71) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("DELETE FROM messages WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0);");
database.execSQL("DELETE FROM messageRecipients WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0);");
database.execSQL("DELETE FROM teachers WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0);");
database.execSQL("DELETE FROM endpointTimers WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0);");
database.execSQL("DELETE FROM metadata WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0) AND thingType = 8;");
database.execSQL("UPDATE profiles SET empty = 1 WHERE archived = 0;");
database.execSQL("UPDATE profiles SET lastReceiversSync = 0 WHERE archived = 0;");
database.execSQL("INSERT INTO config (profileId, `key`, value) VALUES (-1, \"runSync\", \"true\");");
}
};
public static AppDb getDatabase(final Context context) {
@ -903,7 +916,8 @@ public abstract class AppDb extends RoomDatabase {
MIGRATION_66_67,
MIGRATION_67_68,
MIGRATION_68_69,
MIGRATION_69_70
MIGRATION_69_70,
MIGRATION_70_71
)
.allowMainThreadQueries()
//.fallbackToDestructiveMigration()

View File

@ -1,17 +0,0 @@
package pl.szczodrzynski.edziennik.data.db.modules.messages;
public class MessageRecipientFull extends MessageRecipient {
public String fullName = null;
public MessageRecipientFull(int profileId, long id, long replyId, long readDate, long messageId) {
super(profileId, id, replyId, readDate, messageId);
}
public MessageRecipientFull(int profileId, long id, long messageId) {
super(profileId, id, messageId);
}
public MessageRecipientFull() {
super();
}
}

View File

@ -0,0 +1,17 @@
package pl.szczodrzynski.edziennik.data.db.modules.messages
import androidx.room.Ignore
import pl.szczodrzynski.edziennik.fixName
class MessageRecipientFull : MessageRecipient {
var fullName: String? = ""
get() {
return field?.fixName() ?: ""
}
@Ignore
constructor(profileId: Int, id: Long, replyId: Long, readDate: Long, messageId: Long) : super(profileId, id, replyId, readDate, messageId) {}
@Ignore
constructor(profileId: Int, id: Long, messageId: Long) : super(profileId, id, messageId) {}
constructor() : super() {}
}

View File

@ -176,7 +176,7 @@ open class Profile : IDrawerProfile {
}
return context.getDrawableFromRes(R.drawable.profile).also {
it.colorFilter = PorterDuffColorFilter(colorFromName(context, name), PorterDuff.Mode.DST_OVER)
it.colorFilter = PorterDuffColorFilter(colorFromName(name), PorterDuff.Mode.DST_OVER)
}
/*if (profileImage == null) {
@ -192,11 +192,11 @@ open class Profile : IDrawerProfile {
try {
ImageHolder(image ?: "")
} catch (_: Exception) {
ImageHolder(R.drawable.profile, colorFromName(context, name))
ImageHolder(R.drawable.profile, colorFromName(name))
}
}
else {
ImageHolder(R.drawable.profile, colorFromName(context, name))
ImageHolder(R.drawable.profile, colorFromName(name))
}
}
override fun applyImageTo(imageView: ImageView) {

View File

@ -1,10 +1,11 @@
package pl.szczodrzynski.edziennik.data.db.modules.subjects;
import java.util.List;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import pl.szczodrzynski.edziennik.utils.Colors;
import java.util.List;
import pl.szczodrzynski.edziennik.ExtensionsKt;
@Entity(tableName = "subjects",
primaryKeys = {"profileId", "subjectId"})
@ -26,7 +27,7 @@ public class Subject {
this.id = id;
this.longName = longName;
this.shortName = shortName;
this.color = Colors.stringToColor(this.longName);
this.color = ExtensionsKt.colorFromName(longName);
}
@Override

View File

@ -1,189 +0,0 @@
package pl.szczodrzynski.edziennik.data.db.modules.teachers;
import android.content.Context;
import android.graphics.Bitmap;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import pl.szczodrzynski.edziennik.R;
import pl.szczodrzynski.edziennik.utils.Utils;
import static pl.szczodrzynski.edziennik.utils.Utils.bs;
import static pl.szczodrzynski.edziennik.utils.Utils.ns;
@Entity(tableName = "teachers",
primaryKeys = {"profileId", "teacherId"})
public class Teacher {
public int profileId;
@ColumnInfo(name = "teacherId")
public long id;
@ColumnInfo(name = "teacherLoginId")
public String loginId = null;
@ColumnInfo(name = "teacherName")
public String name;
@ColumnInfo(name = "teacherSurname")
public String surname;
@ColumnInfo(name = "teacherType")
public int type;
public static final int TYPE_TEACHER = 0; // 1
public static final int TYPE_EDUCATOR = 1; // 2
public static final int TYPE_PEDAGOGUE = 2; // 4
public static final int TYPE_LIBRARIAN = 3; // 8
public static final int TYPE_SECRETARIAT = 4; // 16
public static final int TYPE_PRINCIPAL = 5; // 32
public static final int TYPE_SCHOOL_ADMIN = 6; // 64
// not teachers
public static final int TYPE_SUPER_ADMIN = 7; // 128
public static final int TYPE_STUDENT = 8; // 256
public static final int TYPE_PARENT = 9; // 512
public static final int TYPE_PARENTS_COUNCIL = 10; // 1024
public static final int TYPE_SCHOOL_PARENTS_COUNCIL = 11; // 2048
public static final int TYPE_OTHER = 12; // 4096
public static final int IS_TEACHER_MASK = 0b1111111;
@ColumnInfo(name = "teacherTypeDescription")
public String typeDescription = null;
public boolean isType(int checkingType) {
return (type & (1 << checkingType)) >= 1;
}
public boolean isTeacher() {
return type <= IS_TEACHER_MASK;
}
public void setType(int setType) {
type |= (1 << setType);
}
public void unsetType(int unsetType) {
type &= ~(1 << unsetType);
}
public static String typeString(Context c, int type) {
return typeString(c, type, null);
}
public static String typeString(Context c, int type, String typeDescription) {
String suffix = bs(" - ", typeDescription);
switch (type) {
default:
case TYPE_TEACHER:
return c.getString(R.string.teacher_teacher)+suffix;
case TYPE_PARENTS_COUNCIL:
return c.getString(R.string.teacher_parents_council)+suffix;
case TYPE_SCHOOL_PARENTS_COUNCIL:
return c.getString(R.string.teacher_school_parents_council)+suffix;
case TYPE_PEDAGOGUE:
return c.getString(R.string.teacher_pedagogue)+suffix;
case TYPE_LIBRARIAN:
return c.getString(R.string.teacher_librarian)+suffix;
case TYPE_SCHOOL_ADMIN:
return c.getString(R.string.teacher_school_admin)+suffix;
case TYPE_SUPER_ADMIN:
return c.getString(R.string.teacher_super_admin)+suffix;
case TYPE_SECRETARIAT:
return c.getString(R.string.teacher_secretariat)+suffix;
case TYPE_PRINCIPAL:
return c.getString(R.string.teacher_principal)+suffix;
case TYPE_EDUCATOR:
return c.getString(R.string.teacher_educator)+suffix;
case TYPE_PARENT:
return c.getString(R.string.teacher_parent)+suffix;
case TYPE_STUDENT:
return c.getString(R.string.teacher_student)+suffix;
case TYPE_OTHER:
return c.getString(R.string.teacher_other)+suffix;
}
}
public String getType(Context c) {
return typeString(c, Utils.leftmostSetBit(type), typeDescription);
}
@Ignore
public Bitmap image = null;
@Ignore
public String displayName = null;
@Ignore
public Teacher(int profileId, long id, String name, String surname) {
this.profileId = profileId;
this.id = id;
this.name = name;
this.surname = surname;
}
public Teacher(int profileId, long id, String name, String surname, String loginId) {
this.profileId = profileId;
this.id = id;
this.name = name;
this.surname = surname;
this.loginId = loginId;
}
public String getFullName()
{
return name+" "+surname;
}
public String getFullNameLastFirst()
{
return surname+" "+name;
}
public String getShortName()
{
return (name.length() == 0 ? " " : name.charAt(0))+"."+surname;
}
// USED IN IUCZNIOWIE + getRecipientList
public static Teacher getById(List<Teacher> teacherList, long id)
{
for (Teacher teacher: teacherList) {
if (teacher.id == id)
{
return teacher;
}
}
return null;
}
public static Teacher getByFullName(List<Teacher> teacherList, String name)
{
for (Teacher teacher: teacherList) {
if ((teacher.name + " " + teacher.surname).equals(name))
{
return teacher;
}
}
return null;
}
public static Teacher getByFullNameLastFirst(List<Teacher> teacherList, String name)
{
for (Teacher teacher: teacherList) {
if ((teacher.surname + " " + teacher.name).equals(name))
{
return teacher;
}
}
return null;
}
public static Teacher getByShortName(List<Teacher> teacherList, String shortName) /* F.Lastname */
{
for (Teacher teacher: teacherList) {
if (teacher.name.length() > 0 && ((teacher.name.charAt(0) + "." + teacher.surname).equals(shortName)))
{
return teacher;
}
}
return null;
}
@NonNull
@Override
public String toString() {
return getFullName();
}
}

View File

@ -0,0 +1,212 @@
package pl.szczodrzynski.edziennik.data.db.modules.teachers
import android.content.Context
import android.graphics.Bitmap
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.fixName
import pl.szczodrzynski.edziennik.join
import java.util.*
@Entity(tableName = "teachers",
primaryKeys = ["profileId", "teacherId"])
open class Teacher {
companion object {
const val TYPE_TEACHER = 0 // 1
const val TYPE_EDUCATOR = 1 // 2
const val TYPE_PEDAGOGUE = 2 // 4
const val TYPE_LIBRARIAN = 3 // 8
const val TYPE_SECRETARIAT = 4 // 16
const val TYPE_PRINCIPAL = 5 // 32
const val TYPE_SCHOOL_ADMIN = 6 // 64
// not teachers
const val TYPE_SPECIALIST = 7 // 128
const val TYPE_SUPER_ADMIN = 10 // 1024
const val TYPE_STUDENT = 12 // 4096
const val TYPE_PARENT = 14 // 16384
const val TYPE_PARENTS_COUNCIL = 15 // 32768
const val TYPE_SCHOOL_PARENTS_COUNCIL = 16 // 65536
const val TYPE_OTHER = 24 // 16777216
const val IS_TEACHER_MASK = 127
val types: List<Int> by lazy { listOf(
TYPE_TEACHER,
TYPE_EDUCATOR,
TYPE_PEDAGOGUE,
TYPE_LIBRARIAN,
TYPE_SECRETARIAT,
TYPE_PRINCIPAL,
TYPE_SCHOOL_ADMIN,
TYPE_SPECIALIST,
TYPE_SUPER_ADMIN,
TYPE_STUDENT,
TYPE_PARENT,
TYPE_PARENTS_COUNCIL,
TYPE_SCHOOL_PARENTS_COUNCIL,
TYPE_OTHER
) }
fun typeName(c: Context, type: Int, typeDescription: String? = null): String {
val suffix = typeDescription?.let { " ($typeDescription)" } ?: ""
return when (type) {
TYPE_TEACHER -> c.getString(R.string.teacher_teacher)
TYPE_PARENTS_COUNCIL -> c.getString(R.string.teacher_parents_council) + suffix
TYPE_SCHOOL_PARENTS_COUNCIL -> c.getString(R.string.teacher_school_parents_council)
TYPE_PEDAGOGUE -> c.getString(R.string.teacher_pedagogue)
TYPE_LIBRARIAN -> c.getString(R.string.teacher_librarian)
TYPE_SCHOOL_ADMIN -> c.getString(R.string.teacher_school_admin)
TYPE_SUPER_ADMIN -> c.getString(R.string.teacher_super_admin)
TYPE_SECRETARIAT -> c.getString(R.string.teacher_secretariat)
TYPE_PRINCIPAL -> c.getString(R.string.teacher_principal)
TYPE_EDUCATOR -> c.getString(R.string.teacher_educator) + suffix
TYPE_PARENT -> c.getString(R.string.teacher_parent) + suffix
TYPE_STUDENT -> c.getString(R.string.teacher_student) + suffix
TYPE_SPECIALIST -> c.getString(R.string.teacher_specialist)
else -> c.getString(R.string.teacher_other) + suffix
}
}
}
var profileId: Int
@ColumnInfo(name = "teacherId")
var id: Long
@ColumnInfo(name = "teacherLoginId")
var loginId: String? = null
@ColumnInfo(name = "teacherName")
var name: String? = ""
@ColumnInfo(name = "teacherSurname")
var surname: String? = ""
@ColumnInfo(name = "teacherType")
var type = 0
@ColumnInfo(name = "teacherTypeDescription")
var typeDescription: String? = null
fun isType(checkingType: Int): Boolean {
return type and (1 shl checkingType) >= 1
}
val isTeacher: Boolean
get() = type <= IS_TEACHER_MASK
fun setTeacherType(i: Int) {
type = type or (1 shl i)
}
fun unsetTeacherType(i: Int) {
type = type and (1 shl i).inv()
}
fun getTypeText(c: Context): String {
val list = mutableListOf<String>()
types.forEach {
if (isType(it))
list += typeName(c, it, typeDescription)
}
return list.join(", ")
}
@Ignore
var image: Bitmap? = null
/**
* Used in Message composing - searching in AutoComplete bolds
* the typed part of the full name.
*/
@Ignore
var recipientDisplayName: CharSequence? = null
/**
* Used in Message composing - determining the priority
* of search result, based on the search phrase match
* (beginning of sentence, beginning of word, middle of word).
*/
@Ignore
var recipientWeight: Int = 0
@Ignore
constructor(profileId: Int, id: Long) {
this.profileId = profileId
this.id = id
}
@Ignore
constructor(profileId: Int, id: Long, name: String, surname: String) {
this.profileId = profileId
this.id = id
this.name = name
this.surname = surname
}
constructor(profileId: Int, id: Long, name: String, surname: String, loginId: String?) {
this.profileId = profileId
this.id = id
this.name = name
this.surname = surname
this.loginId = loginId
}
@Ignore
constructor(teacher: Teacher) {
teacher.let {
this.profileId = it.profileId
this.id = it.id
this.loginId = it.loginId
this.name = it.name
this.surname = it.surname
this.type = it.type
this.typeDescription = it.typeDescription
this.image = it.image
this.recipientDisplayName = it.recipientDisplayName
}
}
@delegate:Ignore
val fullName by lazy { "$name $surname".fixName() }
@delegate:Ignore
val fullNameLastFirst by lazy { "$surname $name".fixName() }
val shortName: String
get() = (if (name == null || name?.length == 0) "" else name!![0].toString()) + "." + surname
override fun toString(): String {
return "Teacher{" +
"profileId=" + profileId +
", id=" + id +
", loginId='" + loginId + '\'' +
", name='" + name + '\'' +
", surname='" + surname + '\'' +
", type=" + dumpType() +
", typeDescription='" + typeDescription + '\'' +
'}'
}
private fun dumpType(): String {
val typeList: MutableList<String> = ArrayList()
if (isType(TYPE_TEACHER)) typeList.add("TYPE_TEACHER")
if (isType(TYPE_EDUCATOR)) typeList.add("TYPE_EDUCATOR($typeDescription)")
if (isType(TYPE_PEDAGOGUE)) typeList.add("TYPE_PEDAGOGUE")
if (isType(TYPE_LIBRARIAN)) typeList.add("TYPE_PEDAGOGUE")
if (isType(TYPE_SECRETARIAT)) typeList.add("TYPE_SECRETARIAT")
if (isType(TYPE_PRINCIPAL)) typeList.add("TYPE_PRINCIPAL")
if (isType(TYPE_SCHOOL_ADMIN)) typeList.add("TYPE_SCHOOL_ADMIN")
if (isType(TYPE_SPECIALIST)) typeList.add("TYPE_SPECIALIST")
if (isType(TYPE_SUPER_ADMIN)) typeList.add("TYPE_SUPER_ADMIN")
if (isType(TYPE_STUDENT)) typeList.add("TYPE_STUDENT")
if (isType(TYPE_PARENT)) typeList.add("TYPE_PARENT")
if (isType(TYPE_PARENTS_COUNCIL)) typeList.add("TYPE_PARENTS_COUNCIL")
if (isType(TYPE_SCHOOL_PARENTS_COUNCIL)) typeList.add("TYPE_SCHOOL_PARENTS_COUNCIL")
if (isType(TYPE_OTHER)) typeList.add("TYPE_OTHER($typeDescription)")
return typeList.join(", ")
}
}

View File

@ -1,39 +1,37 @@
package pl.szczodrzynski.edziennik.data.db.modules.teachers;
package pl.szczodrzynski.edziennik.data.db.modules.teachers
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import java.util.List;
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
public interface TeacherDao {
interface TeacherDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void add(Teacher teacher);
fun add(teacher: Teacher)
@Insert(onConflict = OnConflictStrategy.REPLACE)
void addAll(List<Teacher> teacherList);
fun addAll(teacherList: List<Teacher>)
@Insert(onConflict = OnConflictStrategy.IGNORE)
void addAllIgnore(List<Teacher> teacherList);
fun addAllIgnore(teacherList: List<Teacher>)
@Query("DELETE FROM teachers WHERE profileId = :profileId")
void clear(int profileId);
fun clear(profileId: Int)
@Query("SELECT * FROM teachers WHERE profileId = :profileId AND teacherId = :id")
LiveData<Teacher> getById(int profileId, long id);
fun getById(profileId: Int, id: Long): LiveData<Teacher?>
@Query("SELECT * FROM teachers WHERE profileId = :profileId AND teacherId = :id")
Teacher getByIdNow(int profileId, long id);
fun getByIdNow(profileId: Int, id: Long): Teacher?
@Query("SELECT * FROM teachers WHERE profileId = :profileId AND teacherType <= 127 ORDER BY teacherName, teacherSurname ASC")
LiveData<List<Teacher>> getAllTeachers(int profileId);
fun getAllTeachers(profileId: Int): LiveData<List<Teacher>>
@Query("SELECT * FROM teachers WHERE profileId = :profileId ORDER BY teacherName, teacherSurname ASC")
List<Teacher> getAllNow(int profileId);
fun getAllNow(profileId: Int): List<Teacher>
@Query("UPDATE teachers SET teacherLoginId = :loginId WHERE profileId = :profileId AND teacherId = :id")
void updateLoginId(int profileId, long id, String loginId);
fun updateLoginId(profileId: Int, id: Long, loginId: String?)
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-22.
*/
package pl.szczodrzynski.edziennik.ui.modules.base
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.ColorUtils
import com.google.android.material.snackbar.Snackbar
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.navlib.getColorFromAttr
class MainSnackbar(val activity: AppCompatActivity) {
companion object {
private const val TAG = "MainSnackbar"
}
private var snackbar: Snackbar? = null
private lateinit var coordinator: CoordinatorLayout
fun setCoordinator(coordinatorLayout: CoordinatorLayout, showAbove: View? = null) {
this.coordinator = coordinatorLayout
snackbar = Snackbar.make(coordinator, "", Snackbar.LENGTH_INDEFINITE)
snackbar?.setAction(R.string.more) {
}
val bgColor = ColorUtils.compositeColors(
getColorFromAttr(activity, R.attr.colorOnSurface) and 0xcfffffff.toInt(),
getColorFromAttr(activity, R.attr.colorSurface)
)
snackbar?.setBackgroundTint(bgColor)
showAbove?.let { snackbar?.anchorView = it }
}
fun snackbar(text: String, actionText: String? = null, onClick: (() -> Unit)? = null) {
snackbar?.apply {
setText(text)
setAction(actionText) {
onClick?.invoke()
}
show()
}
}
fun dismiss() = snackbar?.dismiss()
}

View File

@ -56,7 +56,6 @@ import pl.szczodrzynski.edziennik.databinding.CardUpdateBinding;
import pl.szczodrzynski.edziennik.databinding.FragmentHomeOldBinding;
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;
import pl.szczodrzynski.edziennik.utils.Utils;
@ -104,7 +103,6 @@ public class HomeFragmentOld extends Fragment {
b.devMode.setVisibility(App.devMode ? View.VISIBLE : View.GONE);
b.composeButton.setOnClickListener((v -> {
startActivity(new Intent(activity, MessagesComposeActivity.class));
}));
b.pruneWorkButton.setOnClickListener((v -> WorkManager.getInstance(app).pruneWork()));

View File

@ -20,6 +20,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.data.api.task.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.databinding.CardHomeDebugBinding
import pl.szczodrzynski.edziennik.dp
@ -28,7 +29,6 @@ import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard
import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment
import pl.szczodrzynski.edziennik.ui.modules.login.LoginLibrusCaptchaActivity
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesComposeActivity
import kotlin.coroutines.CoroutineContext
class HomeDebugCard(
@ -54,10 +54,27 @@ class HomeDebugCard(
}
holder.root += b.root
b.composeButton.onClick {
app.startActivity(Intent(activity, MessagesComposeActivity::class.java));
b.composeNewButton.onClick {
activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE)
}
b.migrate71.onClick {
app.db.compileStatement("DELETE FROM messages WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0);").executeUpdateDelete()
app.db.compileStatement("DELETE FROM messageRecipients WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0);").executeUpdateDelete()
app.db.compileStatement("DELETE FROM teachers WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0);").executeUpdateDelete()
app.db.compileStatement("DELETE FROM endpointTimers WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0);").executeUpdateDelete()
app.db.compileStatement("DELETE FROM metadata WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0) AND thingType = 8;").executeUpdateDelete()
app.db.compileStatement("UPDATE profiles SET empty = 1 WHERE archived = 0;").executeUpdateDelete()
app.db.compileStatement("UPDATE profiles SET lastReceiversSync = 0 WHERE archived = 0;").executeUpdateDelete()
app.profile.lastReceiversSync = 0
app.profile.empty = true
}
b.syncReceivers.onClick {
EdziennikTask.recipientListGet(App.profileId).enqueue(activity)
}
b.pruneWorkButton.onClick {
WorkManager.getInstance(app).pruneWork()
}

View File

@ -19,8 +19,8 @@ import pl.szczodrzynski.edziennik.data.api.models.ApiError;
import pl.szczodrzynski.edziennik.databinding.FragmentLoginIuczniowieBinding;
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar;
import static pl.szczodrzynski.edziennik.data.api.models.AppError.CODE_INVALID_LOGIN;
import static pl.szczodrzynski.edziennik.data.api.models.AppError.CODE_INVALID_SCHOOL_NAME;
import static pl.szczodrzynski.edziennik.data.api.ErrorsKt.ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN;
import static pl.szczodrzynski.edziennik.data.api.ErrorsKt.ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME;
import static pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_IUCZNIOWIE;
public class LoginIuczniowieFragment extends Fragment {
@ -57,10 +57,10 @@ public class LoginIuczniowieFragment extends Fragment {
ApiError error = LoginActivity.error;
if (error != null) {
switch (error.getErrorCode()) {
case CODE_INVALID_SCHOOL_NAME:
case ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME:
b.loginSchoolNameLayout.setError(getString(R.string.login_error_incorrect_school_name));
break;
case CODE_INVALID_LOGIN:
case ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN:
b.loginPasswordLayout.setError(getString(R.string.login_error_incorrect_login_or_password));
break;
}

View File

@ -19,10 +19,10 @@ import pl.szczodrzynski.edziennik.data.api.models.ApiError;
import pl.szczodrzynski.edziennik.databinding.FragmentLoginMobidziennikBinding;
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar;
import static pl.szczodrzynski.edziennik.data.api.models.AppError.CODE_ARCHIVED;
import static pl.szczodrzynski.edziennik.data.api.models.AppError.CODE_INVALID_LOGIN;
import static pl.szczodrzynski.edziennik.data.api.models.AppError.CODE_INVALID_SERVER_ADDRESS;
import static pl.szczodrzynski.edziennik.data.api.models.AppError.CODE_OLD_PASSWORD;
import static pl.szczodrzynski.edziennik.data.api.ErrorsKt.ERROR_LOGIN_MOBIDZIENNIK_WEB_ARCHIVED;
import static pl.szczodrzynski.edziennik.data.api.ErrorsKt.ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_ADDRESS;
import static pl.szczodrzynski.edziennik.data.api.ErrorsKt.ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN;
import static pl.szczodrzynski.edziennik.data.api.ErrorsKt.ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD;
import static pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_MOBIDZIENNIK;
public class LoginMobidziennikFragment extends Fragment {
@ -59,16 +59,16 @@ public class LoginMobidziennikFragment extends Fragment {
ApiError error = LoginActivity.error;
if (error != null) {
switch (error.getErrorCode()) {
case CODE_INVALID_LOGIN:
case ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN:
b.loginPasswordLayout.setError(getString(R.string.login_error_incorrect_login_or_password));
break;
case CODE_OLD_PASSWORD:
case ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD:
b.loginPasswordLayout.setError(getString(R.string.login_error_old_password));
break;
case CODE_ARCHIVED:
case ERROR_LOGIN_MOBIDZIENNIK_WEB_ARCHIVED:
b.loginUsernameLayout.setError(getString(R.string.sync_error_archived));
break;
case CODE_INVALID_SERVER_ADDRESS:
case ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_ADDRESS:
b.loginServerAddressLayout.setError(getString(R.string.login_error_incorrect_address));
break;
}

View File

@ -60,7 +60,7 @@ public class LoginProgressFragment extends Fragment {
nav.navigate(R.id.loginSummaryFragment, null, LoginActivity.navOptions);
}
@Subscribe(threadMode = ThreadMode.MAIN)
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void onSyncErrorEvent(ApiTaskErrorEvent event) {
LoginActivity.error = event.getError();
if (getActivity() == null)

View File

@ -39,10 +39,10 @@ import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar;
import pl.szczodrzynski.edziennik.ui.modules.webpush.QrScannerActivity;
import pl.szczodrzynski.edziennik.utils.Utils;
import static pl.szczodrzynski.edziennik.data.api.models.AppError.CODE_EXPIRED_TOKEN;
import static pl.szczodrzynski.edziennik.data.api.models.AppError.CODE_INVALID_PIN;
import static pl.szczodrzynski.edziennik.data.api.models.AppError.CODE_INVALID_SYMBOL;
import static pl.szczodrzynski.edziennik.data.api.models.AppError.CODE_INVALID_TOKEN;
import static pl.szczodrzynski.edziennik.data.api.ErrorsKt.ERROR_LOGIN_VULCAN_EXPIRED_TOKEN;
import static pl.szczodrzynski.edziennik.data.api.ErrorsKt.ERROR_LOGIN_VULCAN_INVALID_PIN;
import static pl.szczodrzynski.edziennik.data.api.ErrorsKt.ERROR_LOGIN_VULCAN_INVALID_SYMBOL;
import static pl.szczodrzynski.edziennik.data.api.ErrorsKt.ERROR_LOGIN_VULCAN_INVALID_TOKEN;
import static pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_VULCAN;
public class LoginVulcanFragment extends Fragment {
@ -79,16 +79,16 @@ public class LoginVulcanFragment extends Fragment {
ApiError error = LoginActivity.error;
if (error != null) {
switch (error.getErrorCode()) {
case CODE_INVALID_TOKEN:
case ERROR_LOGIN_VULCAN_INVALID_TOKEN:
b.loginTokenLayout.setError(getString(R.string.login_error_incorrect_token));
break;
case CODE_EXPIRED_TOKEN:
case ERROR_LOGIN_VULCAN_EXPIRED_TOKEN:
b.loginTokenLayout.setError(getString(R.string.login_error_expired_token));
break;
case CODE_INVALID_SYMBOL:
case ERROR_LOGIN_VULCAN_INVALID_SYMBOL:
b.loginSymbolLayout.setError(getString(R.string.login_error_incorrect_symbol));
break;
case CODE_INVALID_PIN:
case ERROR_LOGIN_VULCAN_INVALID_PIN:
/*if (!"?".equals(error.errorText)) {
b.loginPinLayout.setError(getString(R.string.login_error_incorrect_pin_format, error.errorText));
break;

View File

@ -34,7 +34,9 @@ import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent.Companion.T
import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent.Companion.TYPE_PROGRESS
import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
import pl.szczodrzynski.edziennik.data.api.task.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_IUCZNIOWIE
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.databinding.MessageFragmentBinding
@ -88,30 +90,6 @@ class MessageFragment : Fragment(), CoroutineScope {
)
b.closeButton.setOnClickListener { activity.navigateUp() }
val messageId = arguments?.getLong("messageId")
if (messageId == null) {
activity.navigateUp()
return
}
launch {
val deferred = async(Dispatchers.Default) {
val msg = app.db.messageDao().getById(App.profileId, messageId)?.also {
it.recipients = app.db.messageRecipientDao().getAllByMessageId(it.profileId, it.id)
if (it.body != null && !it.seen) {
app.db.metadataDao().setSeen(it.profileId, it, true)
}
}
msg
}
val msg = deferred.await() ?: run {
return@launch
}
message = msg
b.subject.text = message.subject
checkMessage()
}
// click to expand subject and sender
b.subject.onClick {
it.maxLines = if (it.maxLines == 30) 2 else 30
@ -119,6 +97,56 @@ class MessageFragment : Fragment(), CoroutineScope {
b.sender.onClick {
it.maxLines = if (it.maxLines == 30) 2 else 30
}
b.replyButton.onClick {
activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE, Bundle(
"message" to app.gson.toJson(message),
"type" to "reply"
))
}
b.forwardButton.onClick {
activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE, Bundle(
"message" to app.gson.toJson(message),
"type" to "forward"
))
}
launch {
val messageString = arguments?.getString("message")
val messageId = arguments?.getLong("messageId")
if (messageId == null) {
activity.navigateUp()
return@launch
}
val msg = withContext(Dispatchers.Default) {
val msg =
if (messageString != null)
app.gson.fromJson(messageString, MessageFull::class.java)?.also {
//it.body = null
it.addedDate = arguments?.getLong("sentDate") ?: System.currentTimeMillis()
}
else
app.db.messageDao().getById(App.profileId, messageId)
msg?.also {
it.recipients = app.db.messageRecipientDao().getAllByMessageId(it.profileId, it.id)
if (it.body != null && !it.seen) {
app.db.metadataDao().setSeen(it.profileId, it, true)
}
}
} ?: run {
activity.navigateUp()
return@launch
}
message = msg
b.subject.text = message.subject
checkMessage()
}
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
@ -146,7 +174,7 @@ class MessageFragment : Fragment(), CoroutineScope {
// if a sent msg is not read by everyone, download it again to check the read status
if (!checkRecipients()) {
if (!checkRecipients() && app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN) {
EdziennikTask.messageGet(App.profileId, message).enqueue(activity)
return
}
@ -157,7 +185,7 @@ class MessageFragment : Fragment(), CoroutineScope {
private fun checkRecipients(): Boolean {
message.recipients?.forEach { recipient ->
if (recipient.id == -1L)
recipient.fullName = app.profile.accountNameLong ?: app.profile.studentNameLong
recipient.fullName = app.profile.accountNameLong ?: app.profile.studentNameLong ?: ""
if (message.type == TYPE_SENT && recipient.readDate < 1)
return false
}
@ -174,6 +202,22 @@ class MessageFragment : Fragment(), CoroutineScope {
b.subject.text = message.subject
b.replyButton.visibility = if (message.type == TYPE_RECEIVED) View.VISIBLE else View.INVISIBLE
if (message.type == TYPE_RECEIVED) {
activity.navView.apply {
bottomBar.apply {
fabEnable = true
fabExtendedText = getString(R.string.messages_reply)
fabIcon = CommunityMaterial.Icon2.cmd_reply
}
setFabOnClickListener(View.OnClickListener {
b.replyButton.performClick()
})
}
activity.gainAttentionFAB()
}
val messageRecipients = StringBuilder("<ul>")
message.recipients?.forEach { recipient ->
when (recipient.readDate) {

View File

@ -1,201 +0,0 @@
package pl.szczodrzynski.edziennik.ui.modules.messages;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MotionEvent;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import com.afollestad.materialdialogs.MaterialDialog;
import com.hootsuite.nachos.ChipConfiguration;
import com.hootsuite.nachos.NachoTextView;
import com.hootsuite.nachos.chip.Chip;
import com.hootsuite.nachos.chip.ChipInfo;
import com.hootsuite.nachos.chip.ChipSpan;
import com.hootsuite.nachos.chip.ChipSpanChipCreator;
import com.hootsuite.nachos.tokenizer.SpanChipTokenizer;
import com.hootsuite.nachos.validator.IllegalCharacterIdentifier;
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.util.ArrayList;
import java.util.List;
import pl.szczodrzynski.edziennik.App;
import pl.szczodrzynski.edziennik.R;
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher;
import pl.szczodrzynski.edziennik.databinding.ActivityComposeMessageBinding;
import pl.szczodrzynski.edziennik.utils.Colors;
import pl.szczodrzynski.edziennik.utils.Themes;
public class MessagesComposeActivity extends AppCompatActivity {
private static final String TAG = "MessageCompose";
private App app;
private ActivityComposeMessageBinding b;
private List<Teacher> teachers = new ArrayList<>();
private ActionBar actionBar;
private MessagesComposeInfo composeInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
app = (App)getApplication();
setTheme(Themes.INSTANCE.getAppTheme());
b = DataBindingUtil.inflate(getLayoutInflater(), R.layout.activity_compose_message, null, false);
setContentView(b.getRoot());
/*composeInfo = Edziennik.getApi(app, app.profile.getLoginStoreType()).getComposeInfo(app.profile);
Toolbar toolbar = b.toolbar;
setSupportActionBar(toolbar);
actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(R.string.messages_compose_title);
}
List<Teacher> categories = new ArrayList<>();
for (int i = 0; i < 11; i++) {
categories.add(new Teacher(-1, -1*i, Teacher.typeString(this, i), ""));
}
Edziennik.getApi(app, app.profile.getLoginStoreType()).getRecipientList(this, new SyncCallback() {
@Override public void onLoginFirst(List<Profile> profileList, LoginStore loginStore) { }
@Override public void onSuccess(Context activityContext, ProfileFull profileFull) { }
@Override public void onProgress(int progressStep) { }
@Override public void onActionStarted(int stringResId) { }
@Override
public void onError(Context activityContext, AppError error) {
new Handler(activityContext.getMainLooper()).post(() -> {
app.apiEdziennik.guiShowErrorDialog(MessagesComposeActivity.this, error, R.string.messages_recipient_list_download_error);
});
}
}, app.profile, teacherList -> {
teachers.clear();
for (Teacher teacher: teacherList) {
if (teacher.loginId != null)
teachers.add(teacher);
}
teachers.addAll(categories);
MessagesComposeSuggestionAdapter adapter = new MessagesComposeSuggestionAdapter(this, teachers);
//ArrayAdapter<Teacher> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, teachers);
b.nachoTextView.setAdapter(adapter);
});*/
/*app.db.teacherDao().getAllTeachers(App.profileId).observe(this, teachers -> {
});*/
/*int[][] states = new int[][] {
new int[] {}
};
int[] colors = new int[] {
getResources().getColor(ThemeUtils.getChipColorRes())
};*/
//ColorStateList chipStateList = new ColorStateList(states, colors);
b.nachoTextView.setChipTokenizer(new SpanChipTokenizer<>(this, new ChipSpanChipCreator() {
@Override
public ChipSpan createChip(@NonNull Context context, @NonNull CharSequence text, Object data) {
Teacher teacher = (Teacher) data;
if (teacher.id <= 0) {
int type = (int) (teacher.id * -1);
List<Teacher> category = new ArrayList<>();
List<String> categoryNames = new ArrayList<>();
for (Teacher teacher1: teachers) {
if (teacher1.isType(type)) {
category.add(teacher1);
categoryNames.add(teacher1.getFullName());
}
}
new MaterialDialog.Builder(MessagesComposeActivity.this)
.title(R.string.messages_compose_recipients_title)
.content(getString(R.string.messages_compose_recipients_text_format, Teacher.typeString(MessagesComposeActivity.this, type)))
.items(categoryNames)
.itemsCallbackMultiChoice(null, ((dialog, which, text1) -> {
List<ChipInfo> chipInfoList = new ArrayList<>();
for (int index: which) {
Teacher selected = category.get(index);
selected.image = MessagesUtils.getProfileImage(48, 24, 16, 12, 1, selected.getFullName());
chipInfoList.add(new ChipInfo(selected.getFullName(), selected));
}
b.nachoTextView.addTextWithChips(chipInfoList);
return true;
}))
.positiveText(R.string.ok)
.negativeText(R.string.cancel)
.show();
return null;
}
ChipSpan chipSpan = new ChipSpan(context, text, new BitmapDrawable(context.getResources(), teacher.image), teacher);
chipSpan.setIconBackgroundColor(Colors.stringToMaterialColor(teacher.getFullName()));
return chipSpan;
}
@Override
public void configureChip(@NonNull ChipSpan chip, @NonNull ChipConfiguration chipConfiguration) {
super.configureChip(chip, chipConfiguration);
//chip.setBackgroundColor(chipStateList);
chip.setTextColor(Themes.INSTANCE.getPrimaryTextColor(MessagesComposeActivity.this));
}
}, ChipSpan.class));
b.nachoTextView.setIllegalCharacterIdentifier(new IllegalCharacterIdentifier() {
@Override
public boolean isCharacterIllegal(Character c) {
return c.toString().matches("[\\n;:_ ]");
}
});
//b.nachoTextView.addChipTerminator('\n', ChipTerminatorHandler.BEHAVIOR_CHIPIFY_ALL);
//b.nachoTextView.addChipTerminator(' ', ChipTerminatorHandler.BEHAVIOR_CHIPIFY_TO_TERMINATOR);
//b.nachoTextView.addChipTerminator(';', ChipTerminatorHandler.BEHAVIOR_CHIPIFY_CURRENT_TOKEN);
//b.nachoTextView.setNachoValidator(new ChipifyingNachoValidator());
//b.nachoTextView.enableEditChipOnTouch(false, false);
//b.nachoTextView.disableEditChipOnTouch();
b.nachoTextView.setOnChipClickListener(new NachoTextView.OnChipClickListener() {
@Override
public void onChipClick(Chip chip, MotionEvent motionEvent) {
Toast.makeText(app, "onChipClick: " + chip.getText(), Toast.LENGTH_SHORT).show();
}
});
b.nachoTextView.setOnChipRemoveListener(new NachoTextView.OnChipRemoveListener() {
@Override
public void onChipRemove(Chip chip) {
Log.d(TAG, "onChipRemoved: " + chip.getText());
b.nachoTextView.setSelection(b.nachoTextView.getText().length());
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_compose, menu);
menu.findItem(R.id.action_send).setIcon(
new IconicsDrawable(this, CommunityMaterial.Icon2.cmd_send)
.actionBar()
.color(IconicsColor.colorInt(Color.WHITE))
.size(IconicsSize.dp(20))
);
menu.findItem(R.id.action_attachment).setIcon(
new IconicsDrawable(this, CommunityMaterial.Icon.cmd_attachment)
.actionBar()
.color(IconicsColor.colorInt(Color.WHITE))
.size(IconicsSize.dp(20))
);
menu.findItem(R.id.action_attachment).setVisible(composeInfo.maxAttachmentNumber != 0);
return true;
}
}

View File

@ -0,0 +1,476 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-22.
*/
package pl.szczodrzynski.edziennik.ui.modules.messages
import android.content.Context
import android.graphics.Typeface
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.text.HtmlCompat
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hootsuite.nachos.ChipConfiguration
import com.hootsuite.nachos.chip.ChipInfo
import com.hootsuite.nachos.chip.ChipSpan
import com.hootsuite.nachos.chip.ChipSpanChipCreator
import com.hootsuite.nachos.tokenizer.SpanChipTokenizer
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_MESSAGE_NOT_SENT
import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent
import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.task.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage
import pl.szczodrzynski.edziennik.utils.Colors
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.Themes.getPrimaryTextColor
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.navlib.elevateSurface
import java.util.*
import kotlin.coroutines.CoroutineContext
import kotlin.text.replace
class MessagesComposeFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "MessagesComposeFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: MessagesComposeFragmentBinding
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private var teachers = mutableListOf<Teacher>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: 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 = MessagesComposeFragmentBinding.inflate(inflater)
return b.root
}
override fun onDestroy() {
EventBus.getDefault().unregister(this)
super.onDestroy()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
return
EventBus.getDefault().register(this)
b.breakpoints.visibility = if (App.devMode) View.VISIBLE else View.GONE
b.breakpoints.setOnClickListener {
b.breakpoints.isEnabled = true
b.breakpoints.text = "Breakpoints!"
// do your job
}
/*b.fontStyleBold.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_bold)
.sizeDp(24)
.colorAttr(activity, R.attr.colorOnPrimary)
b.fontStyleItalic.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_italic)
.sizeDp(24)
.colorAttr(activity, R.attr.colorOnPrimary)
b.fontStyleUnderline.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_underline)
.sizeDp(24)
.colorAttr(activity, R.attr.colorOnPrimary)
b.fontStyle.addOnButtonCheckedListener { _, checkedId, isChecked ->
val span: Any = when (checkedId) {
R.id.fontStyleBold -> StyleSpan(Typeface.BOLD)
R.id.fontStyleItalic -> StyleSpan(Typeface.ITALIC)
R.id.fontStyleUnderline -> UnderlineSpan()
else -> StyleSpan(Typeface.NORMAL)
}
if (isChecked) {
val flags = if (b.text.selectionStart == b.text.selectionEnd)
SpannableString.SPAN_INCLUSIVE_INCLUSIVE
else
SpannableString.SPAN_EXCLUSIVE_INCLUSIVE
b.text.text?.setSpan(span, b.text.selectionStart, b.text.selectionEnd, flags)
}
else {
b.text.text?.getSpans(b.text.selectionStart, b.text.selectionEnd, span.javaClass)?.forEach {
if (it is StyleSpan && span is StyleSpan && it.style == span.style)
b.text.text?.removeSpan(it)
else if (it.javaClass == span.javaClass)
b.text.text?.removeSpan(it)
}
}
}*/
launch {
delay(100)
getRecipientList()
createView()
}
}
private fun getRecipientList() {
if (System.currentTimeMillis() - app.profile.lastReceiversSync > 1 * DAY * 1000 && app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN) {
activity.snackbar("Pobieranie listy odbiorców...")
EdziennikTask.recipientListGet(App.profileId).enqueue(activity)
}
else {
launch {
val list = withContext(Dispatchers.Default) {
app.db.teacherDao().getAllNow(App.profileId).filter { it.loginId != null }
}
updateRecipientList(list)
}
}
}
private fun createView() {
b.recipientsLayout.setBoxCornerRadii(0f, 0f, 0f, 0f)
b.subjectLayout.setBoxCornerRadii(0f, 0f, 0f, 0f)
b.textLayout.setBoxCornerRadii(0f, 0f, 0f, 0f)
b.recipients.addTextChangedListener(onTextChanged = { _, _, _, _ ->
b.recipientsLayout.error = null
})
b.subject.addTextChangedListener(onTextChanged = { _, _, _, _ ->
b.subjectLayout.error = null
})
b.text.addTextChangedListener(onTextChanged = { _, _, _, _ ->
b.textLayout.error = null
})
b.subjectLayout.counterMaxLength = when (app.profile.loginStoreType) {
LoginStore.LOGIN_TYPE_MOBIDZIENNIK -> 100
LoginStore.LOGIN_TYPE_LIBRUS -> 150
LoginStore.LOGIN_TYPE_VULCAN -> 200
LoginStore.LOGIN_TYPE_IUCZNIOWIE -> 180
LoginStore.LOGIN_TYPE_EDUDZIENNIK -> 0
else -> -1
}
b.textLayout.counterMaxLength = when (app.profile.loginStoreType) {
LoginStore.LOGIN_TYPE_MOBIDZIENNIK -> -1
LoginStore.LOGIN_TYPE_LIBRUS -> 20000
LoginStore.LOGIN_TYPE_VULCAN -> -1
LoginStore.LOGIN_TYPE_IUCZNIOWIE -> 1983
LoginStore.LOGIN_TYPE_EDUDZIENNIK -> 0
else -> -1
}
b.recipients.chipTokenizer = SpanChipTokenizer<ChipSpan>(activity, object : ChipSpanChipCreator() {
override fun createChip(context: Context, text: CharSequence, data: Any?): ChipSpan? {
val teacher = data as Teacher?
if (teacher!!.id in -24L..-1L) {
val type = (teacher.id * -1).toInt()
val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(activity)
val textColorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
val category: MutableList<Teacher> = ArrayList()
val categoryNames: MutableList<CharSequence> = ArrayList()
teachers.forEach {
if (it.isType(type)) {
category += it
val name = it.fullName
val description = when (type) {
Teacher.TYPE_TEACHER -> null
Teacher.TYPE_PARENTS_COUNCIL -> it.typeDescription
Teacher.TYPE_SCHOOL_PARENTS_COUNCIL -> null
Teacher.TYPE_PEDAGOGUE -> null
Teacher.TYPE_LIBRARIAN -> null
Teacher.TYPE_SCHOOL_ADMIN -> null
Teacher.TYPE_SUPER_ADMIN -> null
Teacher.TYPE_SECRETARIAT -> null
Teacher.TYPE_PRINCIPAL -> null
Teacher.TYPE_EDUCATOR -> it.typeDescription
Teacher.TYPE_PARENT -> it.typeDescription
Teacher.TYPE_STUDENT -> it.typeDescription
Teacher.TYPE_SPECIALIST -> null
else -> it.typeDescription
}
categoryNames += listOfNotNull(
name.asSpannable(
ForegroundColorSpan(textColorPrimary)
),
description?.asSpannable(
ForegroundColorSpan(textColorSecondary),
AbsoluteSizeSpan(14.dp)
)
).concat("\n")
}
}
MaterialAlertDialogBuilder(activity)
.setTitle("Dodaj odbiorców - "+Teacher.typeName(activity, type))
//.setMessage(getString(R.string.messages_compose_recipients_text_format, Teacher.typeName(activity, type)))
.setPositiveButton("OK", null)
.setNeutralButton("Anuluj", null)
.setMultiChoiceItems(categoryNames.toTypedArray(), categoryNames.map { false }.toBooleanArray()) { dialog, which, isChecked ->
val chipInfoList = mutableListOf<ChipInfo>()
val selected = category[which]
selected.image = getProfileImage(48, 24, 16, 12, 1, selected.fullName)
chipInfoList.add(ChipInfo(selected.fullName, selected))
b.recipients.addTextWithChips(chipInfoList)
}
.show()
/*MaterialDialog.Builder(activity)
.title(R.string.messages_compose_recipients_title)
.content(getString(R.string.messages_compose_recipients_text_format, Teacher.typeName(activity, type)))
.items(categoryNames)
.itemsCallbackMultiChoice(null) { dialog: MaterialDialog?, which: Array<Int>, text1: Array<CharSequence?>? ->
val chipInfoList = mutableListOf<ChipInfo>()
for (index in which) {
val selected = category[index]
selected.image = getProfileImage(48, 24, 16, 12, 1, selected.fullName)
chipInfoList.add(ChipInfo(selected.fullName, selected))
}
b.recipients.addTextWithChips(chipInfoList)
true
}
.positiveText(R.string.ok)
.negativeText(R.string.cancel)
.show()*/
return null
}
val chipSpan = ChipSpan(context, teacher.fullName, BitmapDrawable(context.resources, teacher.image), teacher)
chipSpan.setIconBackgroundColor(Colors.stringToMaterialColor(teacher.fullName))
return chipSpan
}
override fun configureChip(chip: ChipSpan, chipConfiguration: ChipConfiguration) {
super.configureChip(chip, chipConfiguration)
chip.setBackgroundColor(elevateSurface(activity, 8).toColorStateList())
chip.setTextColor(getPrimaryTextColor(activity))
}
}, ChipSpan::class.java)
b.recipients.setIllegalCharacterIdentifier { c ->
c.toString().matches("[\\n;:_ ]".toRegex())
}
b.recipients.setOnChipClickListener { chip, _ ->
Toast.makeText(app, "onChipClick: " + chip.text, Toast.LENGTH_SHORT).show()
}
b.recipients.setOnChipRemoveListener { chip ->
Log.d(TAG, "onChipRemoved: " + chip.text)
b.recipients.setSelection(b.recipients.text.length)
}
b.recipientsLayout.isEnabled = false
b.subjectLayout.isEnabled = false
b.textLayout.isEnabled = false
activity.navView.apply {
bottomBar.apply {
fabEnable = true
fabExtendedText = getString(R.string.messages_compose_send)
fabIcon = CommunityMaterial.Icon2.cmd_send
}
setFabOnClickListener(View.OnClickListener {
sendMessage()
})
}
activity.gainAttentionFAB()
}
private fun updateRecipientList(list: List<Teacher>) { launch {
withContext(Dispatchers.Default) {
teachers = list.sortedBy { it.fullName }.toMutableList()
Teacher.types.mapTo(teachers) {
Teacher(-1, -it.toLong(), Teacher.typeName(activity, it), "")
}
/*teachers.forEach {
println(it)
}*/
}
b.recipientsLayout.isEnabled = true
b.subjectLayout.isEnabled = true
b.textLayout.isEnabled = true
val adapter = MessagesComposeSuggestionAdapter(activity, teachers)
b.recipients.setAdapter(adapter)
handleReplyMessage()
}}
private fun handleReplyMessage() { launch {
val replyMessage = arguments?.getString("message")
if (replyMessage != null) {
val chipList = mutableListOf<ChipInfo>()
var subject = ""
val span = SpannableStringBuilder()
var body: CharSequence = ""
withContext(Dispatchers.Default) {
val msg = app.gson.fromJson(replyMessage, MessageFull::class.java)
val dateString = getString(R.string.messages_date_time_format, Date.fromMillis(msg.addedDate).formattedStringShort, Time.fromMillis(msg.addedDate).stringHM)
// add original message info
span.appendText("W dniu ")
span.appendSpan(dateString, StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
span.appendText(", ")
span.appendSpan(msg.senderFullName.fixName(), StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
span.appendText(" napisał(a):")
span.setSpan(StyleSpan(Typeface.BOLD), 0, span.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
span.appendText("\n\n")
if (arguments?.getString("type") == "reply") {
// add greeting text
span.replace(0, 0, "\n\nPozdrawiam,\n${app.profile.accountNameLong
?: app.profile.studentNameLong ?: ""}\n\n\n")
teachers.firstOrNull { it.id == msg.senderId }?.let { teacher ->
teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName)
chipList += ChipInfo(teacher.fullName, teacher)
}
subject = "Re: ${msg.subject}"
} else {
span.replace(0, 0, "\n\n")
subject = "Fwd: ${msg.subject}"
}
body = MessagesUtils.htmlToSpannable(msg.body ?: "Nie udało się wczytać oryginalnej wiadomości.")//Html.fromHtml(msg.body?.replace("<br\\s?/?>".toRegex(), "\n") ?: "Nie udało się wczytać oryginalnej wiadomości.")
}
b.recipients.addTextWithChips(chipList)
if (b.recipients.text.isNullOrEmpty())
b.recipients.requestFocus()
else
b.text.requestFocus()
b.subject.setText(subject)
b.text.apply {
text = span.appendText(body)
setSelection(0)
}
b.root.scrollTo(0, 0)
}
else {
b.recipients.requestFocus()
}
}}
private fun sendMessage() {
b.recipientsLayout.error = null
b.subjectLayout.error = null
b.textLayout.error = null
if (b.recipients.tokenValues.isNotEmpty()) {
b.recipientsLayout.error = getString(R.string.messages_compose_recipients_error)
return
}
val recipients = mutableListOf<Teacher>()
b.recipients.allChips.forEach { chip ->
if (chip.data !is Teacher)
return@forEach
val teacher = chip.data as Teacher
recipients += teacher
//println(teacher)
}
val subject = b.subject.text?.toString()
val text = b.text.text
if (recipients.isEmpty()) {
b.recipientsLayout.error = getString(R.string.messages_compose_recipients_empty)
return
}
if (subject.isNullOrBlank() || subject.length < 3) {
b.subjectLayout.error = getString(R.string.messages_compose_subject_empty)
return
}
if (text.isNullOrBlank() || text.length < 3) {
b.textLayout.error = getString(R.string.messages_compose_text_empty)
return
}
if (b.subjectLayout.counterMaxLength != -1 && b.subject.length() > b.subjectLayout.counterMaxLength)
return
if (b.textLayout.counterMaxLength != -1 && b.text.length() > b.textLayout.counterMaxLength)
return
var textHtml = if (app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN && app.profile.loginStoreType != LoginStore.LOGIN_TYPE_IUCZNIOWIE) {
HtmlCompat.toHtml(SpannableString(text), HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
.replace("\n", "")
.replace(" dir=\"ltr\"", "")
}
else {
text.toString()
}
if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_MOBIDZIENNIK) {
textHtml = textHtml
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "span")
.replace("</p>", "</span>")
.replace("<b>", "<strong>")
.replace("</b>", "</strong>")
.replace("<i>", "<em>")
.replace("</i>", "</em>")
.replace("<u>", "<span style=\"text-decoration: underline;\">")
.replace("</u>", "</span>")
}
activity.bottomSheet.hideKeyboard()
EdziennikTask.messageSend(App.profileId, recipients, subject.trim(), textHtml.trim()).enqueue(activity)
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onRecipientListGetEvent(event: RecipientListGetEvent) {
if (event.profileId != App.profileId)
return
EventBus.getDefault().removeStickyEvent(event)
activity.snackbarDismiss()
updateRecipientList(event.teacherList)
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onMessageSentEvent(event: MessageSentEvent) {
if (event.profileId != App.profileId)
return
EventBus.getDefault().removeStickyEvent(event)
if (event.message == null) {
activity.errorSnackbar.addError(ApiError(TAG, ERROR_MESSAGE_NOT_SENT)).show()
return
}
activity.snackbar(app.getString(R.string.messages_sent_success), app.getString(R.string.ok))
activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle(
"messageId" to (event.message.id ?: -1),
"message" to app.gson.toJson(event.message),
"sentDate" to event.sentDate
))
}
}

View File

@ -1,28 +0,0 @@
package pl.szczodrzynski.edziennik.ui.modules.messages;
public class MessagesComposeInfo {
/**
* 0 means no attachments.
* -1 means unlimited number.
*/
public int maxAttachmentNumber = 0;
/**
* -1 means unlimited size.
*/
public long attachmentSizeLimit = 0;
/**
* -1 means unlimited length.
*/
public int maxSubjectLength = -1;
/**
* -1 means unlimited length.
*/
public int maxBodyLength = -1;
public MessagesComposeInfo(int maxAttachmentNumber, long attachmentSizeLimit, int maxSubjectLength, int maxBodyLength) {
this.maxAttachmentNumber = maxAttachmentNumber;
this.attachmentSizeLimit = attachmentSizeLimit;
this.maxSubjectLength = maxSubjectLength;
this.maxBodyLength = maxBodyLength;
}
}

View File

@ -0,0 +1,21 @@
package pl.szczodrzynski.edziennik.ui.modules.messages
class MessagesComposeInfo(
/**
* 0 means no attachments.
* -1 means unlimited number.
*/
@JvmField var maxAttachmentNumber: Int,
/**
* -1 means unlimited size.
*/
var attachmentSizeLimit: Long,
/**
* -1 means unlimited length.
*/
var maxSubjectLength: Int,
/**
* -1 means unlimited length.
*/
var maxBodyLength: Int
)

View File

@ -1,191 +0,0 @@
package pl.szczodrzynski.edziennik.ui.modules.messages;
import android.content.Context;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Filter;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import pl.szczodrzynski.edziennik.R;
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher;
public class MessagesComposeSuggestionAdapter extends ArrayAdapter<Teacher> {
private Context context;
private List<Teacher> teacherList;
private ArrayList<Teacher> originalList = null;
private ArrayFilter mFilter;
private final Object mLock = new Object();
MessagesComposeSuggestionAdapter(@NonNull Context context, List<Teacher> teacherList) {
super(context, 0, teacherList);
this.context = context;
this.teacherList = teacherList;
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View listItem = convertView;
if(listItem == null)
listItem = LayoutInflater.from(context).inflate(R.layout.messages_compose_suggestion_item, parent, false);
Teacher teacher = teacherList.get(position);
TextView name = listItem.findViewById(R.id.name);
TextView type = listItem.findViewById(R.id.type);
ImageView image = listItem.findViewById(R.id.image);
teacher.image = MessagesUtils.getProfileImage(48, 24, 16, 12, 1, teacher.getFullName());
if (teacher.id <= 0) {
name.setText(Teacher.typeString(context, (int) (teacher.id * -1)));
type.setText(R.string.teachers_browse_category);
image.setImageBitmap(null);
}
else {
if (teacher.displayName == null)
name.setText(teacher.getFullName());
else
name.setText(Html.fromHtml(teacher.displayName));
type.setText(teacher.getType(context));
image.setImageBitmap(teacher.image);
}
return listItem;
}
@Override
public int getCount() {
return teacherList.size();
}
@Nullable
@Override
public Teacher getItem(int position) {
return teacherList.get(position);
}
@Override
public int getPosition(@Nullable Teacher item) {
return teacherList.indexOf(item);
}
@NonNull
@Override
public Filter getFilter() {
if (mFilter == null) {
mFilter = new ArrayFilter();
}
return mFilter;
}
private class TeacherWeighted extends Teacher {
public int weight;
public TeacherWeighted(Teacher teacher, int weight) {
super(teacher.profileId, teacher.id, teacher.name, teacher.surname, teacher.loginId);
this.weight = weight;
this.image = teacher.image;
this.type = teacher.type;
this.typeDescription = teacher.typeDescription;
}
@NonNull
@Override
public String toString() {
return getFullName();
}
}
private Comparator<? super Teacher> comparator = (o1, o2) -> ((TeacherWeighted) o1).weight - ((TeacherWeighted) o2).weight;
private class ArrayFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence prefix) {
final FilterResults results = new FilterResults();
if (originalList == null) {
synchronized (mLock) {
originalList = new ArrayList<>(teacherList);
}
}
if (prefix == null || prefix.length() == 0) {
ArrayList<Teacher> list;
synchronized (mLock) {
list = new ArrayList<>(originalList);
}
results.values = list;
results.count = list.size();
} else {
String prefixString = prefix.toString().toLowerCase();
ArrayList<Teacher> values;
synchronized (mLock) {
values = new ArrayList<>(originalList);
}
int count = values.size();
ArrayList<Teacher> newValues = new ArrayList<>();
for (int i = 0; i < count; i++) {
Teacher teacher = values.get(i);
String teacherFullName = teacher.getFullName().toLowerCase();
teacher.displayName = teacherFullName.replace(prefixString, "<b>"+prefixString+"</b>");
// First match against the whole, non-splitted value
boolean found = false;
if (teacherFullName.startsWith(prefixString)) {
newValues.add(new TeacherWeighted(teacher, 1));
found = true;
} else {
// check if prefix matches any of the words
String[] words = teacherFullName.split(" ");
for (String word : words) {
if (word.startsWith(prefixString)) {
newValues.add(new TeacherWeighted(teacher, 2));
found = true;
break;
}
}
}
// finally check if the prefix matches any part of the name
if (!found && teacherFullName.contains(prefixString)) {
newValues.add(new TeacherWeighted(teacher, 3));
}
}
Collections.sort(newValues, comparator);
results.values = newValues;
results.count = newValues.size();
}
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
//noinspection unchecked
teacherList = (List<Teacher>) results.values;
if (results.count > 0) {
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
}
}

View File

@ -0,0 +1,135 @@
package pl.szczodrzynski.edziennik.ui.modules.messages
import android.content.Context
import android.graphics.Typeface.BOLD
import android.text.style.BackgroundColorSpan
import android.text.style.StyleSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Filter
import android.widget.ImageView
import android.widget.TextView
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.asSpannable
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.resolveAttr
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage
import java.util.*
class MessagesComposeSuggestionAdapter(
context: Context,
val originalList: List<Teacher>
) : ArrayAdapter<Teacher>(context, 0, originalList)
{
private var teacherList = originalList.toList()
private val filter by lazy { ArrayFilter() }
private val comparator by lazy { Comparator { o1: Teacher, o2: Teacher -> o1.recipientWeight - o2.recipientWeight } }
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val listItem = convertView ?: LayoutInflater.from(context).inflate(R.layout.messages_compose_suggestion_item, parent, false)
val teacher = teacherList[position]
val name = listItem.findViewById<TextView>(R.id.name)
val type = listItem.findViewById<TextView>(R.id.type)
val image = listItem.findViewById<ImageView>(R.id.image)
if (teacher.image == null)
teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName)
if (teacher.id in -24L..-1L) {
name.text = Teacher.typeName(context, (teacher.id * -1).toInt())
type.setText(R.string.teachers_browse_category)
image.setImageBitmap(null)
} else {
name.text = teacher.recipientDisplayName ?: teacher.fullName
type.text = teacher.getTypeText(context)
image.setImageBitmap(teacher.image)
}
return listItem
}
override fun getCount(): Int {
return teacherList.size
}
override fun getItem(position: Int): Teacher? {
return teacherList[position]
}
override fun getPosition(item: Teacher?): Int {
return teacherList.indexOf(item)
}
override fun getFilter(): Filter {
return filter
}
inner class ArrayFilter : Filter() {
override fun performFiltering(prefix: CharSequence?): FilterResults {
val results = FilterResults()
if (prefix.isNullOrEmpty()) {
results.values = originalList
results.count = originalList.size
} else {
val prefixString = prefix.toString()
val list = mutableListOf<Teacher>()
originalList.forEach { teacher ->
val teacherFullName = teacher.fullName
teacher.recipientWeight = 0
// First match against the whole, non-split value
var found = false
if (teacherFullName.startsWith(prefixString, ignoreCase = true)) {
teacher.recipientWeight = 1
found = true
} else {
// check if prefix matches any of the words
val words = teacherFullName.split(" ").toTypedArray()
for (word in words) {
if (word.startsWith(prefixString, ignoreCase = true)) {
teacher.recipientWeight = 2
found = true
break
}
}
}
// finally check if the prefix matches any part of the name
if (!found && teacherFullName.contains(prefixString, ignoreCase = true)) {
teacher.recipientWeight = 3
}
if (teacher.recipientWeight != 0) {
teacher.recipientDisplayName = teacherFullName.asSpannable(
StyleSpan(BOLD),
BackgroundColorSpan(R.attr.colorControlHighlight.resolveAttr(context)),
substring = prefixString,
ignoreCase = true
)
list += teacher
}
}
Collections.sort(list, comparator)
results.values = list
results.count = list.size
}
return results
}
override fun publishResults(constraint: CharSequence?, results: FilterResults) {
results.values?.let { teacherList = it as List<Teacher> }
if (results.count > 0) {
notifyDataSetChanged()
} else {
notifyDataSetInvalidated()
}
}
}
}

View File

@ -1,580 +0,0 @@
package pl.szczodrzynski.edziennik.ui.modules.messages;
import android.os.Bundle;
import android.os.Environment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import com.afollestad.materialdialogs.MaterialDialog;
import com.google.android.material.chip.Chip;
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 org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import pl.szczodrzynski.edziennik.App;
import pl.szczodrzynski.edziennik.MainActivity;
import pl.szczodrzynski.edziennik.R;
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull;
import pl.szczodrzynski.edziennik.databinding.MessagesDetailsBinding;
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorDialog;
import pl.szczodrzynski.edziennik.utils.Themes;
import pl.szczodrzynski.edziennik.utils.Utils;
import static pl.szczodrzynski.edziennik.utils.Utils.getStringFromFile;
import static pl.szczodrzynski.edziennik.utils.Utils.readableFileSize;
public class MessagesDetailsFragment extends Fragment {
private long messageId = -1;
private App app = null;
private MainActivity activity = null;
private MessagesDetailsBinding b = null;
private MessageFull message;
private List<Attachment> attachmentList = new ArrayList<>();
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
if (getActivity() == null || getContext() == null)
return null;
app = (App) activity.getApplication();
getContext().getTheme().applyStyle(Themes.INSTANCE.getAppTheme(), true);
if (app.profile == null)
return inflater.inflate(R.layout.fragment_loading, container, false);
// activity, context and profile is valid
b = DataBindingUtil.inflate(inflater, R.layout.messages_details, container, false);
return b.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
if (app == null || app.profile == null || activity == null || b == null || !isAdded())
return;
if (getArguments() != null)
messageId = getArguments().getLong("messageId", -1);
b.messageContent.setVisibility(View.GONE);
/*if (messageId != -1) {
AsyncTask.execute(() -> {
if (app == null || app.profile == null || activity == null || b == null || !isAdded())
return;
MessageFull messageRaw = app.db.messageDao().getById(App.profileId, messageId);
Edziennik.getApi(app, app.profile.getLoginStoreType()).getMessage(activity, new SyncCallback() {
@Override public void onLoginFirst(List<Profile> profileList, LoginStore loginStore) { }
@Override public void onSuccess(Context activityContext, ProfileFull profileFull) { }
@Override public void onProgress(int progressStep) { }
@Override public void onActionStarted(int stringResId) { }
@Override
public void onError(Context activityContext, AppError error) {
new Handler(activityContext.getMainLooper()).post(() -> {
app.apiEdziennik.guiShowErrorDialog(activity, error, R.string.messages_download_error);
});
}
}, app.profile, messageRaw, messageFull -> {
if (app == null || app.profile == null || activity == null || b == null || !isAdded())
return;
message = messageFull;
if (message.body == null) {
return;
}
b.messageBody.setText(Html.fromHtml(message.body.replaceAll("\\[META:[A-z0-9]+;[0-9-]+]", "")));
b.progress.setVisibility(View.GONE);
Anim.fadeIn(b.messageContent, 200, null);
MessagesFragment.Companion.setPageSelection(Math.min(message.type, 1));
MessagesUtils.MessageInfo messageInfo = MessagesUtils.getMessageInfo(app, message, 40, 20, 14, 10);
b.messageProfileBackground.setImageBitmap(messageInfo.profileImage);
b.messageSender.setText(messageInfo.profileName);
b.messageSubject.setText(message.subject);
b.messageDate.setText(getString(R.string.messages_date_time_format, Date.fromMillis(message.addedDate).getFormattedStringShort(), Time.fromMillis(message.addedDate).getStringHM()));
StringBuilder messageRecipients = new StringBuilder("<ul>");
for (MessageRecipientFull recipient: message.recipients) {
if (recipient.readDate == -1) messageRecipients.append(getString(
R.string.messages_recipients_list_unknown_state_format,
recipient.fullName
));
else if (recipient.readDate == 0) messageRecipients.append(getString(
R.string.messages_recipients_list_unread_format,
recipient.fullName
));
else if (recipient.readDate == 1) messageRecipients.append(getString(
R.string.messages_recipients_list_read_unknown_date_format,
recipient.fullName
));
else messageRecipients.append(getString(
R.string.messages_recipients_list_read_format,
recipient.fullName,
Date.fromMillis(recipient.readDate).getFormattedString(),
Time.fromMillis(recipient.readDate).getStringHM()
));
}
messageRecipients.append("</ul>");
b.messageRecipients.setText(Html.fromHtml(messageRecipients.toString()));
if (message.attachmentIds != null) {
// there are some attachments: attachmentIds, attachmentNames, attachmentSizes
ViewGroup insertPoint = b.messageAttachments;
FrameLayout.LayoutParams chipLayoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
chipLayoutParams.setMargins(0, Utils.dpToPx(8), 0, Utils.dpToPx(8));
FrameLayout.LayoutParams progressLayoutParams = new FrameLayout.LayoutParams(Utils.dpToPx(18), Utils.dpToPx(18));
progressLayoutParams.setMargins(Utils.dpToPx(8), 0, Utils.dpToPx(8), 0);
progressLayoutParams.gravity = END | CENTER_VERTICAL;
// CREATE VIEWS AND AN OBJECT FOR EVERY ATTACHMENT
int attachmentIndex = 0;
for (String attachmentName: message.attachmentNames) {
long messageId = message.id;
long attachmentId = message.attachmentIds.get(attachmentIndex);
long attachmentSize = message.attachmentSizes.get(attachmentIndex);
// create the parent
FrameLayout attachmentLayout = new FrameLayout(b.getRoot().getContext());
Chip attachmentChip = new Chip(attachmentLayout.getContext());
//attachmentChip.setChipBackgroundColorResource(ThemeUtils.getChipColorRes());
attachmentChip.setLayoutParams(chipLayoutParams);
attachmentChip.setHeight(Utils.dpToPx(40));
// show the file size or not
if (attachmentSize == -1)
attachmentChip.setText(getString(R.string.messages_attachment_no_size_format, attachmentName));
else
attachmentChip.setText(getString(R.string.messages_attachment_format, attachmentName, readableFileSize(attachmentSize)));
attachmentChip.setEllipsize(TextUtils.TruncateAt.MIDDLE);
// create an icon for the attachment
IIcon icon = CommunityMaterial.Icon.cmd_file;
switch (Utils.getExtensionFromFileName(attachmentName)) {
case "txt":
icon = CommunityMaterial.Icon.cmd_file_document;
break;
case "doc":
case "docx":
case "odt":
case "rtf":
icon = CommunityMaterial.Icon.cmd_file_word;
break;
case "xls":
case "xlsx":
case "ods":
icon = CommunityMaterial.Icon.cmd_file_excel;
break;
case "ppt":
case "pptx":
case "odp":
icon = CommunityMaterial.Icon.cmd_file_powerpoint;
break;
case "pdf":
icon = CommunityMaterial.Icon.cmd_file_pdf;
break;
case "mp3":
case "wav":
case "aac":
icon = CommunityMaterial.Icon.cmd_file_music;
break;
case "mp4":
case "avi":
case "3gp":
case "mkv":
case "flv":
icon = CommunityMaterial.Icon.cmd_file_video;
break;
case "jpg":
case "jpeg":
case "png":
case "bmp":
case "gif":
icon = CommunityMaterial.Icon.cmd_file_image;
break;
case "zip":
case "rar":
case "tar":
case "7z":
icon = CommunityMaterial.Icon.cmd_file_lock;
break;
}
attachmentChip.setChipIcon(new IconicsDrawable(activity).color(IconicsColor.colorRes(R.color.colorPrimary)).icon(icon).size(IconicsSize.dp(26)));
attachmentChip.setCloseIcon(new IconicsDrawable(activity).icon(CommunityMaterial.Icon.cmd_check).size(IconicsSize.dp(18)).color(IconicsColor.colorInt(Utils.getAttr(activity, android.R.attr.textColorPrimary))));
attachmentChip.setCloseIconVisible(false);
// set the object's index in the attachmentList as the tag
attachmentChip.setTag(attachmentIndex);
attachmentChip.setOnClickListener(v -> {
if (v.getTag() instanceof Integer) {
downloadAttachment((Integer) v.getTag());
}
});
attachmentLayout.addView(attachmentChip);
ProgressBar attachmentProgress = new ProgressBar(attachmentLayout.getContext());
attachmentProgress.setLayoutParams(progressLayoutParams);
attachmentProgress.setVisibility(View.GONE);
attachmentLayout.addView(attachmentProgress);
insertPoint.addView(attachmentLayout);
// create an object and add to the list
Attachment a = new Attachment(App.profileId, messageId, attachmentId, attachmentName, attachmentSize, attachmentLayout, attachmentChip, attachmentProgress);
attachmentList.add(a);
// check if the file is already downloaded. Show the check icon if necessary and set `downloaded` to true.
checkAttachment(a);
attachmentIndex++;
}
}
else {
// no attachments found
b.messageAttachmentsTitle.setVisibility(View.GONE);
}
});
});
}*/
// click to expand subject and sender
b.messageSubject.setOnClickListener(v -> {
if (b.messageSubject.getMaxLines() == 2) {
b.messageSubject.setMaxLines(30);
}
else {
b.messageSubject.setMaxLines(2);
}
});
b.messageSender.setOnClickListener(v -> {
if (b.messageSender.getMaxLines() == 3) {
b.messageSender.setMaxLines(30);
}
else {
b.messageSender.setMaxLines(3);
}
});
// message close button
b.messageClose.setImageDrawable(new IconicsDrawable(activity).icon(CommunityMaterial.Icon2.cmd_window_close).color(IconicsColor.colorInt(Utils.getAttr(activity, android.R.attr.textColorSecondary))).size(IconicsSize.dp(12)));
b.messageClose.setOnClickListener(v -> {
activity.navigateUp();
});
// enter, exit transitions
//setExitTransition(new Fade());
/*View content = b.getRoot();
content.setAlpha(0f);
ValueAnimator animator = ObjectAnimator.ofFloat(content, View.ALPHA, 0f, 1f);
animator.setStartDelay(50);
animator.setDuration(150);
animator.start();*/
}
private void checkAttachment(Attachment a) {
File storageDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu");
storageDir.mkdirs();
File attachmentDataFile = new File(storageDir, "."+a.profileId+"_"+a.messageId+"_"+a.attachmentId);
if (attachmentDataFile.exists()) {
try {
String attachmentFileName = getStringFromFile(attachmentDataFile);
File attachmentFile = new File(attachmentFileName);
if (attachmentFile.exists()) {
a.downloaded = attachmentFileName;
a.chip.setCloseIconVisible(true);
}
} catch (Exception e) {
e.printStackTrace();
new ErrorDialog(activity, e);
}
}
}
private void downloadAttachment(int index) {
Attachment a = attachmentList.get(index);
if (a.downloaded != null) {
Utils.openFile(activity, new File(a.downloaded));
return;
}
a.chip.setEnabled(false);
a.chip.setTextColor(Themes.INSTANCE.getSecondaryTextColor(activity));
a.progressBar.setVisibility(View.VISIBLE);
File storageDir = Utils.getStorageDir();
/*Edziennik.getApi(app, app.profile.getLoginStoreType()).getAttachment(activity, new SyncCallback() {
@Override
public void onLoginFirst(List<Profile> profileList, LoginStore loginStore) {
}
@Override
public void onSuccess(Context activityContext, ProfileFull profileFull) {
}
@Override
public void onError(Context activityContext, AppError error) {
new Handler(activityContext.getMainLooper()).post(() -> {
a.progressBar.setVisibility(View.GONE);
a.chip.setEnabled(true);
a.chip.setTextColor(Themes.INSTANCE.getPrimaryTextColor(activity));
a.chip.setCloseIconVisible(false);
app.apiEdziennik.guiShowErrorDialog(activity, error, R.string.messages_attachment_cannot_download);
});
}
@Override
public void onProgress(int progressStep) {
}
@Override
public void onActionStarted(int stringResId) {
}
}, app.profile, message, a.attachmentId, builder ->
builder.callbackThreadMode(im.wangchao.mhttp.ThreadMode.SENDING)
.callback(new FileCallbackHandler(new File(storageDir, a.attachmentName)) {
@Override
public void onSuccess(File file, Response response) {
AttachmentEvent event = new AttachmentEvent();
event.profileId = a.profileId;
event.messageId = a.messageId;
event.attachmentId = a.attachmentId;
event.eventType = AttachmentEvent.TYPE_FINISHED;
event.fileName = file.getAbsolutePath();
try {
File attachmentDataFile = new File(Utils.getStorageDir(), "."+event.profileId+"_"+event.messageId+"_"+event.attachmentId);
Utils.writeStringToFile(attachmentDataFile, event.fileName);
} catch (IOException e) {
event.eventType = AttachmentEvent.TYPE_ERROR;
event.exception = e;
}
finally {
EventBus.getDefault().post(event);
}
}
@Override
public void onFailure(Response response, Throwable throwable) {
AttachmentEvent event = new AttachmentEvent();
event.profileId = a.profileId;
event.messageId = a.messageId;
event.attachmentId = a.attachmentId;
event.eventType = AttachmentEvent.TYPE_ERROR;
event.exception = new Exception(throwable);
EventBus.getDefault().post(event);
}
@Override
public void onProgress(long bytesWritten, long bytesTotal) {
AttachmentEvent event = new AttachmentEvent();
event.profileId = a.profileId;
event.messageId = a.messageId;
event.attachmentId = a.attachmentId;
event.eventType = AttachmentEvent.TYPE_PROGRESS;
event.progress = (float)bytesWritten / (float)bytesTotal * 100.0f;
EventBus.getDefault().post(event);
}
})
.build()
.enqueue());*/
}
@Subscribe(threadMode = ThreadMode.POSTING)
public void downloadAttachmentEvent(AttachmentEvent event) {
try {
for (Attachment a: attachmentList) {
if (a.profileId == event.profileId
&& a.messageId == event.messageId
&& a.attachmentId == event.attachmentId) {
if (event.eventType == AttachmentEvent.TYPE_PROGRESS) {
// show downloading progress
a.chip.setText(getString(R.string.messages_attachment_downloading_format, a.attachmentName, Math.round(event.progress)));
}
else if (event.eventType == AttachmentEvent.TYPE_FINISHED) {
// save the downloaded file name
a.downloaded = event.fileName;
// set the correct name (and size)
if (a.attachmentSize == -1)
a.chip.setText(getString(R.string.messages_attachment_no_size_format, a.attachmentName));
else
a.chip.setText(getString(R.string.messages_attachment_format, a.attachmentName, readableFileSize(a.attachmentSize)));
// hide the progress bar and show a tick icon
a.progressBar.setVisibility(View.GONE);
a.chip.setEnabled(true);
a.chip.setTextColor(Themes.INSTANCE.getPrimaryTextColor(activity));
a.chip.setCloseIconVisible(true);
// open the file
Utils.openFile(activity, new File(a.downloaded));
}
else if (event.eventType == AttachmentEvent.TYPE_ERROR) {
a.progressBar.setVisibility(View.GONE);
a.chip.setEnabled(true);
a.chip.setTextColor(Themes.INSTANCE.getPrimaryTextColor(activity));
a.chip.setCloseIconVisible(false);
new MaterialDialog.Builder(activity)
.title(R.string.messages_attachment_cannot_download)
.content(R.string.messages_attachment_cannot_download_text)
.positiveText(R.string.ok)
.neutralColor(R.string.report)
.onNeutral((dialog, which) -> {
new ErrorDialog(activity, event.exception);
})
.show();
}
}
}
}
catch (Exception e) {
new ErrorDialog(activity, e);
}
}
private class Attachment {
int profileId;
long messageId;
long attachmentId;
String attachmentName;
long attachmentSize;
FrameLayout parent;
Chip chip;
ProgressBar progressBar;
/**
* An absolute path of the downloaded file. {@code null} if not downloaded yet.
*/
String downloaded = null;
public Attachment(int profileId, long messageId, long attachmentId, String attachmentName, long attachmentSize, FrameLayout parent, Chip chip, ProgressBar progressBar) {
this.profileId = profileId;
this.messageId = messageId;
this.attachmentId = attachmentId;
this.attachmentName = attachmentName;
this.attachmentSize = attachmentSize;
this.parent = parent;
this.chip = chip;
this.progressBar = progressBar;
}
}
public static class AttachmentEvent {
public int profileId;
public long messageId;
public long attachmentId;
public static final int TYPE_PROGRESS = 0;
public static final int TYPE_FINISHED = 1;
public static final int TYPE_ERROR = 2;
public int eventType = TYPE_PROGRESS;
public float progress = 0.0f;
public String fileName = null;
public Exception exception = null;
}
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
}
/*if (message.type == TYPE_RECEIVED) {
b.messageSender.setText(message.senderFullName);
}
else if (message.type == TYPE_SENT && message.recipients != null && message.recipients.size() > 0) {
StringBuilder senderText = new StringBuilder();
boolean first = true;
for (MessageRecipientFull recipient: message.recipients) {
if (!first) {
senderText.append(", ");
}
first = false;
senderText.append(recipient.fullName);
}
b.messageSender.setText(senderText.toString());
}
if (message.type == TYPE_RECEIVED) {
if (app.appConfig.teacherImages != null && app.appConfig.teacherImages.size() > 0 && app.appConfig.teacherImages.containsKey(message.senderId)) {
Bitmap profileImage;
profileImage = BitmapFactory.decodeFile(app.getFilesDir().getAbsolutePath()+"/teacher_"+message.senderId+".jpg");
profileImage = ThumbnailUtils.extractThumbnail(profileImage, Math.min(profileImage.getWidth(), profileImage.getHeight()), Math.min(profileImage.getWidth(), profileImage.getHeight()));
RoundedBitmapDrawable roundDrawable = RoundedBitmapDrawableFactory.create(app.getResources(), profileImage);
roundDrawable.setCircular(true);
b.messageProfileImage.setImageDrawable(roundDrawable);
}
else {
int color = Colors.stringToMaterialColor(message.senderFullName);
b.messageProfileBackground.getDrawable().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP));
b.messageProfileName.setTextColor(ColorUtils.blendARGB(Colors.legibleTextColor(color), color, 0.30f));
if (message.senderFullName != null) {
String[] nameParts = message.senderFullName.split(" ");
b.messageProfileName.setText(nameParts[0].toUpperCase().charAt(0) + "" + nameParts[1].toUpperCase().charAt(0));
}
else {
b.messageProfileName.setText("N");
}
}
View.OnClickListener onClickListener = v -> new MaterialDialog.Builder(activity)
.title(R.string.settings_profile_change_title)
.items(
getString(R.string.settings_profile_change_image),
getString(R.string.settings_profile_remove_image)
)
.itemsCallback((dialog, itemView, position, text) -> {
switch (position) {
case 0:
CropImage.activity()
.setAspectRatio(1, 1)
//.setMaxCropResultSize(512, 512)
.setCropShape(CropImageView.CropShape.OVAL)
.setGuidelines(CropImageView.Guidelines.ON)
.start(activity, MessagesDetailsFragment.this);
break;
case 1:
if (app.appConfig.teacherImages != null) {
app.appConfig.teacherImages.remove(message.senderId);
app.saveConfig("teacherImages");
}
break;
}
})
.negativeText(R.string.cancel)
.show();
b.messageSender.setOnClickListener(onClickListener);
b.messageProfileBackground.setOnClickListener(onClickListener);
}*/

View File

@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.ViewPager
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@ -90,6 +91,20 @@ class MessagesFragment : Fragment() {
b.tabLayout.setupWithViewPager(b.viewPager)
activity.navView.apply {
bottomBar.apply {
fabEnable = true
fabExtendedText = getString(R.string.compose)
fabIcon = CommunityMaterial.Icon2.cmd_pencil_outline
}
setFabOnClickListener(View.OnClickListener {
activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE)
})
}
activity.gainAttentionFAB()
/*if (app.profile.loginStoreType == LOGIN_TYPE_LIBRUS && app.profile.getStudentData("accountPassword", null) == null) {
MaterialDialog.Builder(activity)
.title("Wiadomości w systemie Synergia")

View File

@ -46,9 +46,9 @@ public class MessagesListFragment extends Fragment {
static final long TRANSITION_DURATION = 300L;
static final String TAP_POSITION = "tap_position";
private static int[] tapPositions = {NO_POSITION, NO_POSITION};
private static int[] topPositions = {NO_POSITION, NO_POSITION};
private static int[] bottomPositions = {NO_POSITION, NO_POSITION};
public static int[] tapPositions = {NO_POSITION, NO_POSITION};
public static int[] topPositions = {NO_POSITION, NO_POSITION};
public static int[] bottomPositions = {NO_POSITION, NO_POSITION};
private int messageType = Message.TYPE_RECEIVED;

View File

@ -9,21 +9,14 @@ import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.fixName
import pl.szczodrzynski.edziennik.getNameInitials
import pl.szczodrzynski.edziennik.utils.Colors
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.Utils
import kotlin.math.roundToInt
object MessagesUtils {
private fun getInitials(name: String?): String {
if (name == null || name.isEmpty()) return ""
val nameUppercase = name.toUpperCase()
val nameParts = nameUppercase.split(" ").toTypedArray()
return if (nameParts.size <= 1) (if (nameUppercase.isEmpty()) '?' else nameUppercase[0]).toString()
else (if (nameParts[0].isEmpty()) '?' else nameParts[0][0]).toString() +
if (nameParts[1].isEmpty()) "?" else nameParts[1][0]
}
private fun getPaintCenter(textPaint: Paint): Int {
return ((textPaint.descent() + textPaint.ascent()) / 2).roundToInt()
}
@ -52,7 +45,7 @@ object MessagesUtils {
textPaint.color = ColorUtils.blendARGB(Colors.legibleTextColor(color), color, 0.30f)
textPaint.textSize = textSizeBig
canvas.drawArc(rectF, 0f, 360f, true, circlePaint)
canvas.drawText(getInitials(name), diameter / 2, diameter / 2 - getPaintCenter(textPaint), textPaint)
canvas.drawText(name.getNameInitials(), diameter / 2, diameter / 2 - getPaintCenter(textPaint), textPaint)
}
count == 2 -> { // top
name = names[0]
@ -60,14 +53,14 @@ object MessagesUtils {
textPaint.color = ColorUtils.blendARGB(Colors.legibleTextColor(color), color, 0.30f)
textPaint.textSize = textSizeMedium
canvas.drawArc(rectF, 180f, 180f, true, circlePaint)
canvas.drawText(getInitials(name), diameter / 2, diameter / 4 - getPaintCenter(textPaint), textPaint)
canvas.drawText(name.getNameInitials(), diameter / 2, diameter / 4 - getPaintCenter(textPaint), textPaint)
// bottom
name = names[1]
circlePaint.color = Colors.stringToMaterialColor(name).also { color = it }
textPaint.color = ColorUtils.blendARGB(Colors.legibleTextColor(color), color, 0.30f)
textPaint.textSize = textSizeMedium
canvas.drawArc(rectF, 0f, 180f, true, circlePaint)
canvas.drawText(getInitials(name), diameter / 2, diameter / 4 * 3 - getPaintCenter(textPaint), textPaint)
canvas.drawText(name.getNameInitials(), diameter / 2, diameter / 4 * 3 - getPaintCenter(textPaint), textPaint)
}
count == 3 -> { // upper left
name = names[0]
@ -75,21 +68,21 @@ object MessagesUtils {
textPaint.color = ColorUtils.blendARGB(Colors.legibleTextColor(color), color, 0.30f)
textPaint.textSize = textSizeSmall
canvas.drawArc(rectF, 180f, 90f, true, circlePaint)
canvas.drawText(getInitials(name), diameter / 4, diameter / 4 - getPaintCenter(textPaint) + diameter / 32, textPaint)
canvas.drawText(name.getNameInitials(), diameter / 4, diameter / 4 - getPaintCenter(textPaint) + diameter / 32, textPaint)
// upper right
name = names[1]
circlePaint.color = Colors.stringToMaterialColor(name).also { color = it }
textPaint.color = ColorUtils.blendARGB(Colors.legibleTextColor(color), color, 0.30f)
textPaint.textSize = textSizeSmall
canvas.drawArc(rectF, 270f, 90f, true, circlePaint)
canvas.drawText(getInitials(name), diameter / 4 * 3, diameter / 4 - getPaintCenter(textPaint) + diameter / 32, textPaint)
canvas.drawText(name.getNameInitials(), diameter / 4 * 3, diameter / 4 - getPaintCenter(textPaint) + diameter / 32, textPaint)
// bottom
name = names[2]
circlePaint.color = Colors.stringToMaterialColor(name).also { color = it }
textPaint.color = ColorUtils.blendARGB(Colors.legibleTextColor(color), color, 0.30f)
textPaint.textSize = textSizeMedium
canvas.drawArc(rectF, 0f, 180f, true, circlePaint)
canvas.drawText(getInitials(name), diameter / 2, diameter / 4 * 3 - getPaintCenter(textPaint), textPaint)
canvas.drawText(name.getNameInitials(), diameter / 2, diameter / 4 * 3 - getPaintCenter(textPaint), textPaint)
}
count >= 4 -> { // upper left
name = names[0]
@ -97,21 +90,21 @@ object MessagesUtils {
textPaint.color = ColorUtils.blendARGB(Colors.legibleTextColor(color), color, 0.30f)
textPaint.textSize = textSizeSmall
canvas.drawArc(rectF, 180f, 90f, true, circlePaint)
canvas.drawText(getInitials(name), diameter / 4, diameter / 4 - getPaintCenter(textPaint) + diameter / 32, textPaint)
canvas.drawText(name.getNameInitials(), diameter / 4, diameter / 4 - getPaintCenter(textPaint) + diameter / 32, textPaint)
// upper right
name = names[1]
circlePaint.color = Colors.stringToMaterialColor(name).also { color = it }
textPaint.color = ColorUtils.blendARGB(Colors.legibleTextColor(color), color, 0.30f)
textPaint.textSize = textSizeSmall
canvas.drawArc(rectF, 270f, 90f, true, circlePaint)
canvas.drawText(getInitials(name), diameter / 4 * 3, diameter / 4 - getPaintCenter(textPaint) + diameter / 32, textPaint)
canvas.drawText(name.getNameInitials(), diameter / 4 * 3, diameter / 4 - getPaintCenter(textPaint) + diameter / 32, textPaint)
// bottom left
name = names[2]
circlePaint.color = Colors.stringToMaterialColor(name).also { color = it }
textPaint.color = ColorUtils.blendARGB(Colors.legibleTextColor(color), color, 0.30f)
textPaint.textSize = textSizeSmall
canvas.drawArc(rectF, 90f, 90f, true, circlePaint)
canvas.drawText(getInitials(name), diameter / 4, diameter / 4 * 3 - getPaintCenter(textPaint) - diameter / 32, textPaint)
canvas.drawText(name.getNameInitials(), diameter / 4, diameter / 4 * 3 - getPaintCenter(textPaint) - diameter / 32, textPaint)
// bottom right
if (count == 4) name = names[3]
if (count > 4) name = "..."
@ -119,7 +112,7 @@ object MessagesUtils {
textPaint.color = ColorUtils.blendARGB(Colors.legibleTextColor(color), color, 0.30f)
textPaint.textSize = textSizeSmall
canvas.drawArc(rectF, 0f, 90f, true, circlePaint)
canvas.drawText(if (count > 4) "+" + (count - 3) else getInitials(name), diameter / 4 * 3, diameter / 4 * 3 - getPaintCenter(textPaint) - diameter / 32, textPaint)
canvas.drawText(if (count > 4) "+" + (count - 3) else name.getNameInitials(), diameter / 4 * 3, diameter / 4 * 3 - getPaintCenter(textPaint) - diameter / 32, textPaint)
}
}
return bitmap
@ -129,8 +122,8 @@ object MessagesUtils {
var profileImage: Bitmap? = null
var profileName: String? = null
if (message.type == Message.TYPE_RECEIVED || message.type == Message.TYPE_DELETED) {
profileName = message.senderFullName
profileImage = getProfileImage(diameterDp, textSizeBigDp, textSizeMediumDp, textSizeSmallDp, 1, message.senderFullName)
profileName = message.senderFullName?.fixName()
profileImage = getProfileImage(diameterDp, textSizeBigDp, textSizeMediumDp, textSizeSmallDp, 1, profileName)
} else if (message.type == Message.TYPE_SENT || message.type == Message.TYPE_DRAFT && message.recipients != null) {
when (val count = message.recipients?.size ?: 0) {
0 -> {

View File

@ -84,6 +84,11 @@ public class Colors {
0xFF6D4C41
};
/**
* Used for teacher's images (e.g. in messages or announcements).
* @param s teacher's fullName
* @return a material color based on the fullName hash
*/
public static int stringToMaterialColor(String s) {
long seed = 1;
try {

View File

@ -17,6 +17,7 @@ data class NavTarget(
var isStatic: Boolean = false
var isBelowSeparator: Boolean = false
var popToHome: Boolean = false
var popTo: Int? = null
var badgeTypeId: Int? = null
var canHideInDrawer: Boolean = true
var canHideInMiniDrawer: Boolean = true
@ -64,6 +65,11 @@ data class NavTarget(
return this
}
fun withPopTo(popTo: Int): NavTarget {
this.popTo = popTo
return this
}
fun withBadgeTypeId(badgeTypeId: Int?): NavTarget {
this.badgeTypeId = badgeTypeId
return this

View File

@ -1,70 +0,0 @@
<?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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.modules.messages.MessagesComposeActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
tools:title="@string/messages_compose_title" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.hootsuite.nachos.NachoTextView
android:id="@+id/nacho_text_view"
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/messages_compose_to_hint" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/messages_compose_subject_hint" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<View
android:id="@+id/toolbar_elevation"
android:layout_width="match_parent"
android:layout_height="5dp"
android:background="@drawable/shadow_top"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_collapseMode="pin"
tools:visibility="visible" />
</FrameLayout>
</LinearLayout>
</layout>

View File

@ -32,6 +32,12 @@
android:layout_height="wrap_content"
android:text="Librus Captcha" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Network"
android:textSize="16sp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/runChucker"
style="@style/Widget.MaterialComponents.Button"
@ -39,31 +45,38 @@
android:layout_height="wrap_content"
android:text="Launch Chucker" />
<LinearLayout
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
android:text="Messages"
android:textSize="16sp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/composeButton"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:text="Compose" />
<com.google.android.material.button.MaterialButton
android:id="@+id/composeNewButton"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Compose 2" />
<com.google.android.material.button.MaterialButton
android:id="@+id/composeNewButton"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:text="Compose 2" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/migrate71"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Migration 71" />
<com.google.android.material.button.MaterialButton
android:id="@+id/syncReceivers"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sync receivers" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sync"
android:textSize="16sp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/pruneWorkButton"

View File

@ -68,7 +68,8 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:orientation="vertical"
android:paddingBottom="40dp">
<LinearLayout
android:layout_width="match_parent"
@ -253,10 +254,11 @@
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:orientation="horizontal"
android:visibility="gone"
android:visibility="visible"
tools:visibility="visible">
<LinearLayout
android:id="@+id/replyButton"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="4dp"
@ -270,6 +272,15 @@
android:paddingRight="4dp"
android:paddingBottom="8dp">
<com.mikepenz.iconics.view.IconicsImageView
android:id="@+id/replyIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="4dp"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-reply"
tools:srcCompat="@android:drawable/ic_menu_revert" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -277,16 +288,10 @@
android:text="Odpowiedz"
android:textAppearance="@style/NavView.TextView.Small" />
<com.mikepenz.iconics.view.IconicsImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="4dp"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-reply"
tools:srcCompat="@android:drawable/ic_menu_revert" />
</LinearLayout>
<LinearLayout
android:id="@+id/forwardButton"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="4dp"
@ -300,6 +305,15 @@
android:paddingRight="4dp"
android:paddingBottom="8dp">
<com.mikepenz.iconics.view.IconicsImageView
android:id="@+id/forwardIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="4dp"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-arrow-right-thick"
tools:srcCompat="@android:drawable/ic_media_ff" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -308,13 +322,6 @@
android:textAllCaps="false"
android:textAppearance="@style/NavView.TextView.Small" />
<com.mikepenz.iconics.view.IconicsImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="4dp"
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-share"
tools:srcCompat="@android:drawable/ic_media_ff" />
</LinearLayout>
<LinearLayout
@ -329,14 +336,8 @@
android:paddingLeft="4dp"
android:paddingTop="8dp"
android:paddingRight="4dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Usuń"
android:textAppearance="@style/NavView.TextView.Small" />
android:paddingBottom="8dp"
android:visibility="invisible">
<com.mikepenz.iconics.view.IconicsImageView
android:layout_width="24dp"
@ -345,6 +346,14 @@
app:iiv_color="?android:textColorSecondary"
app:iiv_icon="cmd-delete"
tools:srcCompat="@android:drawable/ic_menu_delete" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Usuń"
android:textAppearance="@style/NavView.TextView.Small" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-12-22.
-->
<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">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="40dp"
android:orientation="vertical"><!-- half of the FAB's size -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/recipientsLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:boxBackgroundColor="@android:color/transparent"
app:boxBackgroundMode="filled">
<com.hootsuite.nachos.NachoTextView
android:id="@+id/recipients"
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:focusedByDefault="true"
android:hint="@string/messages_compose_to_hint" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/subjectLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:boxBackgroundColor="@android:color/transparent"
app:boxBackgroundMode="filled"
app:counterEnabled="true"
tools:counterMaxLength="180">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/subject"
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:hint="@string/messages_compose_subject_hint"
android:singleLine="true"
tools:text="kachoomba"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:boxBackgroundColor="@android:color/transparent"
app:boxBackgroundMode="filled"
app:counterEnabled="true"
tools:counterMaxLength="1983">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:minLines="3"
android:hint="@string/messages_compose_text_hint"
android:ems="10"
android:gravity="start|top"
tools:text="Witam,\n\nchciałem przekazać bardzo ważną wiadomość.\nJest to cytat znanej osoby.\n\n&quot;To jest tak, ale nie. Pamiętaj żeby oczywiście&quot;\n\nCytat ma bardzo duże przesłanie i ogromny wpływ na dzisiejszą kulturę i rozwój współczesnej cywilizacji.\n\nJako zadanie domowe, należy wypisać 5 pierwszych liczb pierwszych. Uzasadnij decyzję, odwołując się do cytatu i 3 innych przykładów z literatury lub filmu.\n\nPozdrawiam,\nJa."
android:inputType="textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
<!--<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/fontStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="4dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleBold"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
tools:icon="@sample/format-bold"
app:iconPadding="0dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleItalic"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
tools:icon="@sample/format-bold"
app:iconPadding="0dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleUnderline"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
tools:icon="@sample/format-bold"
app:iconPadding="0dp"/>
</com.google.android.material.button.MaterialButtonToggleGroup>-->
<Button
android:id="@+id/breakpoints"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="Breakpoints!"/>
</LinearLayout>
</ScrollView>
</layout>

View File

@ -254,7 +254,9 @@
android:text="Wyniki sprawdzianu z matematyki.pdf"
android:visibility="visible"
app:chipIcon="@drawable/googleg_standard_color_18"
app:chipMinHeight="36dp" />
app:chipMinHeight="36dp"
app:closeIcon="@drawable/background_transparent"
app:closeIconVisible="true"/>
<ProgressBar
android:id="@+id/progressBar3"

View File

@ -5,6 +5,7 @@
<resources>
<string name="error_1" translatable="false">ERROR_APP_CRASH</string>
<string name="error_10" translatable="false">ERROR_MESSAGE_NOT_SENT</string>
<string name="error_50" translatable="false">ERROR_REQUEST_FAILURE</string>
<string name="error_51" translatable="false">ERROR_REQUEST_HTTP_400</string>
@ -13,7 +14,13 @@
<string name="error_54" translatable="false">ERROR_REQUEST_HTTP_404</string>
<string name="error_55" translatable="false">ERROR_REQUEST_HTTP_405</string>
<string name="error_56" translatable="false">ERROR_REQUEST_HTTP_410</string>
<string name="error_57" translatable="false">ERROR_REQUEST_HTTP_500</string>
<string name="error_57" translatable="false">ERROR_REQUEST_HTTP_424</string>
<string name="error_58" translatable="false">ERROR_REQUEST_HTTP_500</string>
<string name="error_59" translatable="false">ERROR_REQUEST_HTTP_503</string>
<string name="error_60" translatable="false">ERROR_REQUEST_FAILURE_HOSTNAME_NOT_FOUND</string>
<string name="error_61" translatable="false">ERROR_REQUEST_FAILURE_TIMEOUT</string>
<string name="error_62" translatable="false">ERROR_REQUEST_FAILURE_NO_INTERNET</string>
<string name="error_63" translatable="false">ERROR_REQUEST_FAILURE_SSL_ERROR</string>
<string name="error_100" translatable="false">ERROR_RESPONSE_EMPTY</string>
<string name="error_101" translatable="false">ERROR_LOGIN_DATA_MISSING</string>
<string name="error_102" translatable="false">ERROR_LOGIN_DATA_INVALID</string>
@ -81,6 +88,7 @@
<string name="error_179" translatable="false">ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN</string>
<string name="error_180" translatable="false">ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN</string>
<string name="error_181" translatable="false">ERROR_LIBRUS_API_MAINTENANCE</string>
<string name="error_182" translatable="false">ERROR_LIBRUS_PORTAL_MAINTENANCE</string>
<string name="error_201" translatable="false">ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN</string>
<string name="error_202" translatable="false">ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD</string>
@ -129,6 +137,7 @@
<string name="error_441" translatable="false">ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA</string>
<string name="error_450" translatable="false">ERROR_IDZIENNIK_API_ACCESS_DENIED</string>
<string name="error_451" translatable="false">ERROR_IDZIENNIK_API_OTHER</string>
<string name="error_452" translatable="false">ERROR_IDZIENNIK_API_NO_REGISTER</string>
<string name="error_501" translatable="false">ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN</string>
<string name="error_510" translatable="false">ERROR_LOGIN_EDUDZIENNIK_WEB_OTHER</string>
@ -148,7 +157,9 @@
<string name="error_905" translatable="false">EXCEPTION_LIBRUS_SYNERGIA_REQUEST</string>
<string name="error_906" translatable="false">EXCEPTION_MOBIDZIENNIK_WEB_REQUEST</string>
<string name="error_907" translatable="false">EXCEPTION_VULCAN_API_REQUEST</string>
<string name="error_910" translatable="false">EXCEPTION_NOTIFY_AND_SYNC</string>
<string name="error_908" translatable="false">EXCEPTION_MOBIDZIENNIK_WEB_FILE_REQUEST</string>
<string name="error_909" translatable="false">EXCEPTION_LIBRUS_MESSAGES_FILE_REQUEST</string>
<string name="error_910" translatable="false">EXCEPTION_NOTIFY</string>
<string name="error_911" translatable="false">EXCEPTION_LIBRUS_MESSAGES_REQUEST</string>
<string name="error_912" translatable="false">EXCEPTION_IDZIENNIK_WEB_REQUEST</string>
<string name="error_913" translatable="false">EXCEPTION_IDZIENNIK_WEB_API_REQUEST</string>
@ -159,6 +170,7 @@
<string name="error_1201" translatable="false">LOGIN_NO_ARGUMENTS</string>
<string name="error_1_reason">Aplikacja przestała działać</string>
<string name="error_10_reason">Nie udało się wysłać wiadomości: nowa wiadomość nie została odnaleziona na liście wiadomości wysłanych</string>
<string name="error_50_reason">Błąd odpowiedzi serwera</string>
<string name="error_51_reason">Błąd serwera: nieprawidłowe zapytanie</string>
@ -167,7 +179,13 @@
<string name="error_54_reason">Błąd serwera: plik nie znaleziony</string>
<string name="error_55_reason">Błąd serwera: nieprawidłowa metoda zapytania</string>
<string name="error_56_reason">Błąd serwera: odpowiedź niedostępna</string>
<string name="error_57_reason">Wewnętrzny błąd serwera</string>
<string name="error_57_reason">Błąd serwera: niespełnione zależności</string>
<string name="error_58_reason">Wewnętrzny błąd serwera</string>
<string name="error_59_reason">Usługa tymczasowo niedostępna</string>
<string name="error_60_reason">Brak internetu: nie znaleziono adresu serwera</string>
<string name="error_61_reason">Brak internetu: przekroczono czas oczekiwania</string>
<string name="error_62_reason">Brak internetu</string>
<string name="error_63_reason">Brak internetu: połączenie SSL nie powiodło się</string>
<string name="error_100_reason">Brak odpowiedzi serwera</string>
<string name="error_101_reason">Dane logowania niekompletne</string>
<string name="error_102_reason">Nieprawidłowe dane logowania</string>
@ -235,6 +253,7 @@
<string name="error_179_reason">Librus Wiadomości: nieprawidłowe dane logowania</string>
<string name="error_180_reason">Librus Portal: nieprawidłowe dane logowania</string>
<string name="error_181_reason">Librus API: przerwa techniczna</string>
<string name="error_182_reason">Librus Portal: przerwa techniczna</string>
<string name="error_201_reason">ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN</string>
<string name="error_202_reason">ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD</string>
@ -283,6 +302,7 @@
<string name="error_441_reason">ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA</string>
<string name="error_450_reason">ERROR_IDZIENNIK_API_ACCESS_DENIED</string>
<string name="error_451_reason">ERROR_IDZIENNIK_API_OTHER</string>
<string name="error_452_reason">ERROR_IDZIENNIK_API_NO_REGISTER</string>
<string name="error_501_reason">Błędny email lub hasło</string>
<string name="error_510_reason">Inny błąd logowania</string>
@ -302,7 +322,9 @@
<string name="error_905_reason">EXCEPTION_LIBRUS_SYNERGIA_REQUEST</string>
<string name="error_906_reason">EXCEPTION_MOBIDZIENNIK_WEB_REQUEST</string>
<string name="error_907_reason">EXCEPTION_VULCAN_API_REQUEST</string>
<string name="error_910_reason">EXCEPTION_NOTIFY_AND_SYNC</string>
<string name="error_908_reason">EXCEPTION_MOBIDZIENNIK_WEB_FILE_REQUEST</string>
<string name="error_909_reason">EXCEPTION_LIBRUS_MESSAGES_FILE_REQUEST</string>
<string name="error_910_reason">EXCEPTION_NOTIFY</string>
<string name="error_911_reason">EXCEPTION_LIBRUS_MESSAGES_REQUEST</string>
<string name="error_912_reason">EXCEPTION_IDZIENNIK_WEB_REQUEST</string>
<string name="error_913_reason">EXCEPTION_IDZIENNIK_WEB_API_REQUEST</string>

View File

@ -483,6 +483,7 @@
<string name="messages_compose_title">Napisz wiadomość</string>
<string name="messages_compose_to_hint">Do</string>
<string name="messages_date_time_format" translatable="false">%s, %s</string>
<string name="messages_reply_date_time_format">%s o %s</string>
<string name="messages_download_error">Błąd pobierania wiadomości</string>
<string name="messages_draft_title">Wersja robocza</string>
<string name="messages_recipient_list_download_error">Błąd pobierania listy odbiorców</string>
@ -1125,6 +1126,8 @@
<string name="widget_timetable_no_lessons_found">Brak lekcji przez następne 7 dni.</string>
<string name="widget_timetable_short_no_timetable">Nie pobrano planu lekcji.</string>
<string name="widget_timetable_short_no_lessons">Brak lekcji przez nast. 7 dni.</string>
<string name="messages_compose_text_hint">Napisz wiadomość...</string>
<string name="menu_message_compose">Napisz wiadomość</string>
<string name="no_lessons_today">Nie ma dzisiaj żadnych lekcji!</string>
<string name="lessons_finished">Nie ma dzisiaj więcej lekcji!</string>
<string name="login_edudziennik_title">Zaloguj się - Edudziennik</string>
@ -1133,4 +1136,14 @@
<string name="dialog_error_details_title">Szczegóły błędu</string>
<string name="edziennik_progress_endpoint_descriptive_grades">Pobieranie ocen opisowych…</string>
<string name="edziennik_progress_endpoint_point_grades">Pobieranie ocen punktowych…</string>
<string name="teacher_specialist">Specjaliści</string>
<string name="messages_compose_send">Wyślij</string>
<string name="messages_compose_recipients_error">Sprawdź poprawność wybranych odbiorców</string>
<string name="messages_compose_recipients_empty">Wybierz odbiorców</string>
<string name="messages_compose_subject_empty">Wpisz temat o długości co najmniej 3 znaków</string>
<string name="messages_compose_text_empty">Wpisz treść wiadomości</string>
<string name="messages_sent_success">Wiadomość została wysłana</string>
<string name="compose">Napisz</string>
<string name="messages_reply">Odpowiedz</string>
<string name="error_unknown_format">Kod błędu: %d (w %s)</string>
</resources>

View File

@ -69,6 +69,23 @@
<item name="android:windowNoTitle">true</item>
</style>
<style name="AppTheme.MaterialAlertDialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
<item name="materialAlertDialogTitleTextStyle">@style/AppTheme.MaterialAlertDialog.TitleText</item>
<item name="materialAlertDialogBodyTextStyle">@style/AppTheme.MaterialAlertDialog.BodyText</item>
</style>
<style name="AppTheme.MaterialAlertDialog.TitleText" parent="MaterialAlertDialog.MaterialComponents.Title.Text">
<item name="textSize">20sp</item>
<item name="android:textSize">20sp</item>
<item name="textColor">?android:textColorPrimary</item>
<item name="android:textColor">?android:textColorPrimary</item>
</style>
<style name="AppTheme.MaterialAlertDialog.BodyText">
<item name="textSize">16sp</item>
<item name="android:textSize">16sp</item>
<item name="textColor">?android:textColorSecondary</item>
<item name="android:textColor">?android:textColorSecondary</item>
</style>
<style name="AppTheme" parent="NavView" />
<!-- Base application theme. -->
@ -104,6 +121,8 @@
<item name="timetable_lesson_shifted_target_color">#4caf50</item>
<item name="hourDividerColor">#b0b0b0</item>
<item name="halfHourDividerColor">#e0e0e0</item>
<item name="materialAlertDialogTheme">@style/AppTheme.MaterialAlertDialog</item>
</style>
<style name="AppTheme.Dark" parent="NavView.Dark">
<item name="colorPrimary">#64b5f6</item>
@ -137,6 +156,8 @@
<item name="timetable_lesson_shifted_target_color">#4caf50</item>
<item name="hourDividerColor">#7fffffff</item>
<item name="halfHourDividerColor">#40ffffff</item>
<item name="materialAlertDialogTheme">@style/AppTheme.MaterialAlertDialog</item>
</style>

BIN
libs/java-json.jar Normal file

Binary file not shown.