Compare commits

...

10 Commits

38 changed files with 1509 additions and 704 deletions

3
.gitignore vendored
View File

@ -86,4 +86,5 @@ app/schemas/
signatures/ signatures/
app/.cxx app/.cxx
/i18n/

View File

@ -41,5 +41,15 @@
<option name="name" value="MavenRepo" /> <option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" /> <option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository> </remote-repository>
<remote-repository>
<option name="id" value="maven4" />
<option name="name" value="maven4" />
<option name="url" value="https://dl.bintray.com/undervoid/Powerpermission" />
</remote-repository>
<remote-repository>
<option name="id" value="maven4" />
<option name="name" value="maven4" />
<option name="url" value="https://dl.bintray.com/undervoid/PowerPermission" />
</remote-repository>
</component> </component>
</project> </project>

View File

@ -198,6 +198,9 @@ dependencies {
kapt project(":codegen") kapt project(":codegen")
implementation 'com.google.android:flexbox:2.0.1' implementation 'com.google.android:flexbox:2.0.1'
implementation 'com.qifan.powerpermission:powerpermission:1.0.0'
implementation 'com.qifan.powerpermission:powerpermission-coroutines:1.0.0'
} }
repositories { repositories {
mavenCentral() mavenCentral()

View File

@ -14,6 +14,9 @@
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- PowerPermission uses minSdk 21, it's safe to override as it is used only in >= 23 -->
<uses-sdk tools:overrideLibrary="com.qifan.powerpermission.coroutines, com.qifan.powerpermission.core" />
<application <application
android:name=".App" android:name=".App"
android:allowBackup="true" android:allowBackup="true"

View File

@ -1,15 +1,10 @@
<h3>Wersja 4.0-rc.5, 2020-04-05</h3> <h3>Wersja 4.0, 2020-04-19</h3>
<ul>
<li>Możliwość pobierania załączników do zadań domowych (Librus, MobiDziennik, iDziennik).</li>
<li>Widok pełnej treści zadań domowych (iDziennik, EduDziennik).</li>
<li>Wyszukiwarka wiadomości, pozwalająca na łatwe znalezienie potrzebnej konwersacji.</li>
<li>Możliwość usuwania odebranych wiadomości w aplikacji.</li>
</ul>
<!--<h3>Wersja 4.0-rc.3, 2020-03-29</h3>
<ul> <ul>
<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><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><b>Przebudowaliśmy cały moduł synchronizacji</b>, co oznacza większą stabilność aplikacji, szybkość oraz poprawność pobieranych danych</li> <li><b>Przebudowaliśmy cały moduł synchronizacji</b>, co oznacza większą stabilność aplikacji, szybkość oraz poprawność pobieranych danych</li>
<li>Udoskonalony wygląd Szkolnego - sprawi, że korzystanie z aplikacji będzie jeszcze przyjemniejsze</li> <li>Udoskonalony wygląd Szkolnego - sprawi, że korzystanie z aplikacji będzie jeszcze przyjemniejsze</li>
<li>Wyszukiwarka wiadomości, pozwalająca na łatwe znalezienie potrzebnej konwersacji.</li>
<li>Możliwość pobierania załączników do zadań domowych oraz wiadomości w każdym dzienniku.</li>
<li>Nowa <b>Strona główna</b> - ładniejszy wygląd oraz możliwość przestawiania kart na każdym profilu</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> <li>Nowy <b>Plan lekcji</b> - z doskonałą obsługą lekcji przesuniętych oraz dwóch lekcji o tej samej godzinie</li>
<li>Nowe <b>Oceny</b> - z możliwością zmiany wartości plusów oraz minusów oraz wyłączenia niektórych ocen ze średniej</li> <li>Nowe <b>Oceny</b> - z możliwością zmiany wartości plusów oraz minusów oraz wyłączenia niektórych ocen ze średniej</li>
@ -17,6 +12,7 @@
<li>Znaczki nieprzeczytanych informacji na obrazkach profili.</li> <li>Znaczki nieprzeczytanych informacji na obrazkach profili.</li>
<br> <br>
<br> <br>
<li>Udoskonalone tłumaczenie na j.angielski (dzięki @Predator)</li>
<li>Nowe okienka informacji o wydarzeniach oraz lekcjach</li> <li>Nowe okienka informacji o wydarzeniach oraz lekcjach</li>
<li>Nowe, przyjemniejsze powiadomienia</li> <li>Nowe, przyjemniejsze powiadomienia</li>
<li>Dużo poprawek w widoku <b>Wiadomości</b> oraz <b>Ogłoszeń</b></li> <li>Dużo poprawek w widoku <b>Wiadomości</b> oraz <b>Ogłoszeń</b></li>
@ -29,7 +25,7 @@
<li>Poprawiliśmy synchronizację w tle na niektórych telefonach</li> <li>Poprawiliśmy synchronizację w tle na niektórych telefonach</li>
<li>Usunąłem denerwujący brak zaznaczenia w lewym menu</li> <li>Usunąłem denerwujący brak zaznaczenia w lewym menu</li>
<li>Znaczna ilość błędów z poprzednich wersji już nie występuje</li> <li>Znaczna ilość błędów z poprzednich wersji już nie występuje</li>
</ul>--> </ul>
<br> <br>
<br> <br>
Dzięki za korzystanie ze Szkolnego!<br> Dzięki za korzystanie ze Szkolnego!<br>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/ /*secret password - removed for source code publication*/
static toys AES_IV[16] = { static toys AES_IV[16] = {
0x7b, 0x51, 0x86, 0xc5, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; 0x38, 0xd4, 0x73, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

View File

@ -65,6 +65,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val gradesManager by lazy { GradesManager(this) } val gradesManager by lazy { GradesManager(this) }
val timetableManager by lazy { TimetableManager(this) } val timetableManager by lazy { TimetableManager(this) }
val eventManager by lazy { EventManager(this) } val eventManager by lazy { EventManager(this) }
val permissionManager by lazy { PermissionManager(this) }
val db val db
get() = App.db get() = App.db
@ -166,6 +167,10 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
db.profileDao().firstId?.let { profileLoadById(it) } db.profileDao().firstId?.let { profileLoadById(it) }
} }
config.ui.language?.let {
setLanguage(it)
}
devMode = BuildConfig.DEBUG devMode = BuildConfig.DEBUG
Signing.getCert(this) Signing.getCert(this)

View File

@ -454,7 +454,7 @@ operator fun MatchResult.get(group: Int): String {
return groupValues[group] return groupValues[group]
} }
fun Activity.setLanguage(language: String) { fun Context.setLanguage(language: String) {
val locale = Locale(language.toLowerCase(Locale.ROOT)) val locale = Locale(language.toLowerCase(Locale.ROOT))
val configuration = resources.configuration val configuration = resources.configuration
Locale.setDefault(locale) Locale.setDefault(locale)
@ -463,7 +463,6 @@ fun Activity.setLanguage(language: String) {
} }
configuration.locale = locale configuration.locale = locale
resources.updateConfiguration(configuration, resources.displayMetrics) resources.updateConfiguration(configuration, resources.displayMetrics)
baseContext.resources.updateConfiguration(configuration, baseContext.resources.displayMetrics)
} }
/* /*

View File

@ -226,8 +226,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
list += NavTarget(TARGET_MESSAGES_DETAILS, R.string.menu_message, MessageFragment::class).withPopTo(DRAWER_ITEM_MESSAGES) 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_MESSAGES_COMPOSE, R.string.menu_message_compose, MessagesComposeFragment::class)
list += NavTarget(TARGET_WEB_PUSH, R.string.menu_web_push, WebPushFragment::class) list += NavTarget(TARGET_WEB_PUSH, R.string.menu_web_push, WebPushFragment::class)
list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class) if (App.debugMode) {
if (App.devMode) { list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class)
list += NavTarget(TARGET_LAB, R.string.menu_lab, LabFragment::class) list += NavTarget(TARGET_LAB, R.string.menu_lab, LabFragment::class)
.withIcon(CommunityMaterial.Icon.cmd_flask_outline) .withIcon(CommunityMaterial.Icon.cmd_flask_outline)
.isInDrawer(true) .isInDrawer(true)
@ -414,8 +414,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
R.color.md_green_500 R.color.md_green_500
) )
isStoragePermissionGranted()
SyncWorker.scheduleNext(app) SyncWorker.scheduleNext(app)
UpdateWorker.scheduleNext(app) UpdateWorker.scheduleNext(app)

View File

@ -111,5 +111,7 @@ const val VULCAN_API_ENDPOINT_MESSAGES_SENT = "mobile-api/Uczen.v3.Uczen/Wiadomo
const val VULCAN_API_ENDPOINT_MESSAGES_CHANGE_STATUS = "mobile-api/Uczen.v3.Uczen/ZmienStatusWiadomosci" 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_MESSAGES_ADD = "mobile-api/Uczen.v3.Uczen/DodajWiadomosc"
const val VULCAN_API_ENDPOINT_PUSH = "mobile-api/Uczen.v3.Uczen/UstawPushToken" const val VULCAN_API_ENDPOINT_PUSH = "mobile-api/Uczen.v3.Uczen/UstawPushToken"
const val VULCAN_API_ENDPOINT_MESSAGES_ATTACHMENTS = "mobile-api/Uczen.v3.Uczen/WiadomosciZalacznik"
const val VULCAN_API_ENDPOINT_HOMEWORK_ATTACHMENTS = "mobile-api/Uczen.v3.Uczen/ZadaniaDomoweZalacznik"
const val EDUDZIENNIK_USER_AGENT = "Szkolny.eu/${BuildConfig.VERSION_NAME}" const val EDUDZIENNIK_USER_AGENT = "Szkolny.eu/${BuildConfig.VERSION_NAME}"

View File

@ -158,6 +158,7 @@ const val ERROR_LOGIN_VULCAN_NO_PUPILS = 331
const val ERROR_VULCAN_API_MAINTENANCE = 340 const val ERROR_VULCAN_API_MAINTENANCE = 340
const val ERROR_VULCAN_API_BAD_REQUEST = 341 const val ERROR_VULCAN_API_BAD_REQUEST = 341
const val ERROR_VULCAN_API_OTHER = 342 const val ERROR_VULCAN_API_OTHER = 342
const val ERROR_VULCAN_ATTACHMENT_DOWNLOAD = 343
const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN = 401 const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN = 401
const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME = 402 const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME = 402
@ -207,5 +208,6 @@ const val EXCEPTION_IDZIENNIK_WEB_API_REQUEST = 913
const val EXCEPTION_IDZIENNIK_API_REQUEST = 914 const val EXCEPTION_IDZIENNIK_API_REQUEST = 914
const val EXCEPTION_EDUDZIENNIK_WEB_REQUEST = 920 const val EXCEPTION_EDUDZIENNIK_WEB_REQUEST = 920
const val EXCEPTION_EDUDZIENNIK_FILE_REQUEST = 921 const val EXCEPTION_EDUDZIENNIK_FILE_REQUEST = 921
const val ERROR_ONEDRIVE_DOWNLOAD = 930
const val LOGIN_NO_ARGUMENTS = 1201 const val LOGIN_NO_ARGUMENTS = 1201

View File

@ -97,7 +97,7 @@ object Regexes {
"""zadanieFormularz\(([0-9]+),""".toRegex(DOT_MATCHES_ALL) """zadanieFormularz\(([0-9]+),""".toRegex(DOT_MATCHES_ALL)
} }
val MOBIDZIENNIK_HOMEWORK_ATTACHMENT by lazy { val MOBIDZIENNIK_HOMEWORK_ATTACHMENT by lazy {
"""zalacznik=([0-9]+)'.+?word-break">(.+?)</td>""".toRegex(DOT_MATCHES_ALL) """zalacznik(_zadania)?=([0-9]+)'.+?word-break">(.+?)</td>""".toRegex(DOT_MATCHES_ALL)
} }

View File

@ -0,0 +1,105 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-7.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.helper
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.FileCallbackHandler
import im.wangchao.mhttp.callback.TextCallbackHandler
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.ERROR_ONEDRIVE_DOWNLOAD
import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE
import pl.szczodrzynski.edziennik.data.api.SYSTEM_USER_AGENT
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.utils.Utils
import java.io.File
class OneDriveDownloadAttachment(
app: App,
fileUrl: String,
val onSuccess: (file: File) -> Unit,
val onProgress: (written: Long, total: Long) -> Unit,
val onError: (apiError: ApiError) -> Unit
) {
companion object {
private const val TAG = "OneDriveDownloadAttachment"
}
init {
Request.builder()
.url(fileUrl)
.userAgent(SYSTEM_USER_AGENT)
.withClient(app.httpLazy)
.callback(object : TextCallbackHandler() {
override fun onSuccess(text: String, response: Response) {
val location = response.headers().get("Location")
// https://onedrive.live.com/redir?resid=D75496A2EB87531C!706&authkey=!ABjZeh3pHMqj11Q
if (location?.contains("onedrive.live.com/redir?resid=") != true) {
onError(ApiError(TAG, ERROR_ONEDRIVE_DOWNLOAD)
.withApiResponse(text)
.withResponse(response))
return
}
val url = location
.replace("onedrive.live.com/redir?resid=", "storage.live.com/items/")
.replace("?", "&")
.replaceFirst("&", "?")
downloadFile(url)
}
override fun onFailure(response: Response, throwable: Throwable) {
onError(ApiError(TAG, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
})
.build()
.enqueue()
}
private fun downloadFile(url: String) {
val targetFile = Utils.getStorageDir()
val callback = object : FileCallbackHandler(targetFile) {
override fun onSuccess(file: File?, response: Response?) {
if (file == null) {
onError(ApiError(TAG, ERROR_ONEDRIVE_DOWNLOAD)
.withResponse(response))
return
}
try {
onSuccess(file)
} catch (e: Exception) {
onError(ApiError(TAG, ERROR_ONEDRIVE_DOWNLOAD)
.withResponse(response)
.withThrowable(e))
}
}
override fun onProgress(bytesWritten: Long, bytesTotal: Long) {
try {
this@OneDriveDownloadAttachment.onProgress(bytesWritten, bytesTotal)
} catch (e: Exception) {
onError(ApiError(TAG, ERROR_ONEDRIVE_DOWNLOAD)
.withThrowable(e))
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
onError(ApiError(TAG, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url(url)
.userAgent(SYSTEM_USER_AGENT)
.callback(callback)
.build()
.enqueue()
}
}

View File

@ -54,13 +54,13 @@ class LibrusSandboxDownloadAttachment(override val data: DataLibrus,
} }
private fun getAttachmentCheckKey(attachmentKey: String, callback: () -> Unit) { private fun getAttachmentCheckKey(attachmentKey: String, callback: () -> Unit) {
sandboxGet(LibrusMessagesGetAttachment.TAG, "CSCheckKey", sandboxGet(TAG, "CSCheckKey",
parameters = mapOf("singleUseKey" to attachmentKey)) { json -> parameters = mapOf("singleUseKey" to attachmentKey)) { json ->
when (json.getString("status")) { when (json.getString("status")) {
"not_downloaded_yet" -> { "not_downloaded_yet" -> {
if (getAttachmentCheckKeyTries++ > 5) { if (getAttachmentCheckKeyTries++ > 5) {
data.error(ApiError(LibrusMessagesGetAttachment.TAG, ERROR_FILE_DOWNLOAD) data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD)
.withApiResponse(json)) .withApiResponse(json))
return@sandboxGet return@sandboxGet
} }
@ -75,7 +75,7 @@ class LibrusSandboxDownloadAttachment(override val data: DataLibrus,
} }
else -> { else -> {
data.error(ApiError(LibrusMessagesGetAttachment.TAG, EXCEPTION_LIBRUS_MESSAGES_REQUEST) data.error(ApiError(TAG, EXCEPTION_LIBRUS_MESSAGES_REQUEST)
.withApiResponse(json)) .withApiResponse(json))
} }
} }
@ -85,7 +85,7 @@ class LibrusSandboxDownloadAttachment(override val data: DataLibrus,
private fun downloadAttachment(url: String, method: Int = GET) { private fun downloadAttachment(url: String, method: Int = GET) {
val targetFile = File(Utils.getStorageDir(), attachmentName) val targetFile = File(Utils.getStorageDir(), attachmentName)
sandboxGetFile(LibrusMessagesGetAttachment.TAG, url, targetFile, { file -> sandboxGetFile(TAG, url, targetFile, { file ->
val event = AttachmentGetEvent( val event = AttachmentGetEvent(
profileId, profileId,

View File

@ -29,14 +29,14 @@ class MobidziennikWebGetAttachment(override val data: DataMobidziennik,
val typeUrl = when (owner) { val typeUrl = when (owner) {
is Message -> if (owner.type == Message.TYPE_SENT) is Message -> if (owner.type == Message.TYPE_SENT)
"dziennik/wiadwyslana?id=" "dziennik/wiadwyslana/?id="
else else
"dziennik/wiadodebrana?id=" "dziennik/wiadodebrana/?id="
is Event -> if (owner.date >= Date.getToday()) is Event -> if (owner.date >= Date.getToday())
"mobile/zadaniadomowe?id_zadania=" "dziennik/wyslijzadanie/?id_zadania="
else else
"mobile/zadaniadomowearchiwalne?id_zadania=" "dziennik/wyslijzadanie/?id_zadania="
else -> "" else -> ""
} }
@ -47,7 +47,7 @@ class MobidziennikWebGetAttachment(override val data: DataMobidziennik,
else -> -1 else -> -1
} }
webGetFile(TAG, "/$typeUrl${ownerId}&zalacznik=$attachmentId", targetFile, { file -> webGetFile(TAG, "/$typeUrl${ownerId}&uczen=${data.studentId}&zalacznik=$attachmentId", targetFile, { file ->
val event = AttachmentGetEvent( val event = AttachmentGetEvent(
profileId, profileId,

View File

@ -39,9 +39,9 @@ class MobidziennikWebGetHomework(override val data: DataMobidziennik,
event.attachmentIds = mutableListOf() event.attachmentIds = mutableListOf()
event.attachmentNames = mutableListOf() event.attachmentNames = mutableListOf()
Regexes.MOBIDZIENNIK_HOMEWORK_ATTACHMENT.findAll(tableRow).onEach { Regexes.MOBIDZIENNIK_HOMEWORK_ATTACHMENT.findAll(tableRow).forEach {
event.attachmentIds?.add(it[1].toLongOrNull() ?: return@onEach) event.attachmentIds?.add(it[2].toLongOrNull() ?: return@forEach)
event.attachmentNames?.add(it[2]) event.attachmentNames?.add(it[3])
} }
event.homeworkBody = "" event.homeworkBody = ""

View File

@ -5,26 +5,31 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan
import com.google.gson.JsonObject import com.google.gson.JsonObject
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.helper.OneDriveDownloadAttachment
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanData import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanData
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiAttachments
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiMessagesChangeStatus import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiMessagesChangeStatus
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiSendMessage 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.firstlogin.VulcanFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLogin import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLogin
import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent
import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent
import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError 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.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.d
import java.io.File
class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface { class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
companion object { companion object {
@ -87,8 +92,29 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
override fun getMessage(message: MessageFull) { override fun getMessage(message: MessageFull) {
login(LOGIN_METHOD_VULCAN_API) { login(LOGIN_METHOD_VULCAN_API) {
VulcanApiMessagesChangeStatus(data, message) { if (message.attachmentIds != null) {
completed() VulcanApiMessagesChangeStatus(data, message) {
completed()
}
return@login
}
val list = data.app.db.messageDao().getAllNow(data.profileId)
VulcanApiAttachments(data, list, message, MessageFull::class) { _ ->
list.forEach {
if (it.attachmentIds == null)
it.attachmentIds = mutableListOf()
data.messageList.add(it)
}
data.messageListReplace = true
if (message.seen) {
EventBus.getDefault().postSticky(MessageGetEvent(message))
completed()
return@VulcanApiAttachments
}
VulcanApiMessagesChangeStatus(data, message) {
completed()
}
} }
} }
} }
@ -103,9 +129,66 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
override fun markAllAnnouncementsAsRead() {} override fun markAllAnnouncementsAsRead() {}
override fun getAnnouncement(announcement: AnnouncementFull) {} override fun getAnnouncement(announcement: AnnouncementFull) {}
override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) {}
override fun getRecipientList() {} override fun getRecipientList() {}
override fun getEvent(eventFull: EventFull) {}
override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) {
val fileUrl = attachmentName.substringAfter(":")
if (attachmentName == fileUrl) {
data.error(ApiError(TAG, ERROR_ONEDRIVE_DOWNLOAD))
return
}
OneDriveDownloadAttachment(
app,
fileUrl,
onSuccess = { file ->
val event = AttachmentGetEvent(
data.profileId,
owner,
attachmentId,
AttachmentGetEvent.TYPE_FINISHED,
file.absolutePath
)
val attachmentDataFile = File(Utils.getStorageDir(), ".${data.profileId}_${event.ownerId}_${event.attachmentId}")
Utils.writeStringToFile(attachmentDataFile, event.fileName)
EventBus.getDefault().postSticky(event)
completed()
},
onProgress = { written, total ->
val event = AttachmentGetEvent(
data.profileId,
owner,
attachmentId,
AttachmentGetEvent.TYPE_PROGRESS,
bytesWritten = written
)
EventBus.getDefault().postSticky(event)
},
onError = { apiError ->
data.error(apiError)
}
)
}
override fun getEvent(eventFull: EventFull) {
login(LOGIN_METHOD_VULCAN_API) {
val list = data.app.db.eventDao().getAllNow(data.profileId).filter { !it.addedManually }
VulcanApiAttachments(data, list, eventFull, EventFull::class) { _ ->
list.forEach {
it.homeworkBody = ""
data.eventList.add(it)
}
data.eventListReplace = true
EventBus.getDefault().postSticky(EventGetEvent(eventFull))
completed()
}
}
}
override fun firstLogin() { VulcanFirstLogin(data) { completed() } } override fun firstLogin() { VulcanFirstLogin(data) { completed() } }
override fun cancel() { override fun cancel() {

View File

@ -0,0 +1,124 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-6.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api
import pl.szczodrzynski.edziennik.asJsonObjectList
import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_HOMEWORK_ATTACHMENTS
import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_MESSAGES_ATTACHMENTS
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.getJsonArray
import pl.szczodrzynski.edziennik.getLong
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.models.Date
import kotlin.reflect.KClass
class VulcanApiAttachments(override val data: DataVulcan,
val list: List<*>,
val owner: Any?,
val ownerClass: KClass<*>,
val onSuccess: (list: List<*>) -> Unit
) : VulcanApi(data, null) {
companion object {
const val TAG = "VulcanApiAttachments"
}
init { run {
val endpoint = when (ownerClass) {
MessageFull::class -> VULCAN_API_ENDPOINT_MESSAGES_ATTACHMENTS
EventFull::class -> VULCAN_API_ENDPOINT_HOMEWORK_ATTACHMENTS
else -> null
} ?: return@run
val idName = when (ownerClass) {
MessageFull::class -> "IdWiadomosc"
EventFull::class -> "IdZadanieDomowe"
else -> null
} ?: return@run
val startDate = profile?.getSemesterStart(profile?.currentSemester ?: 1)?.inUnix ?: 0
val endDate = Date.getToday().stepForward(0, 1, 0).inUnix
apiGet(TAG, endpoint, parameters = mapOf(
"DataPoczatkowa" to startDate,
"DataKoncowa" to endDate,
"LoginId" to data.studentLoginId,
"IdUczen" to data.studentId
)) { json, _ ->
json.getJsonArray("Data")?.asJsonObjectList()?.forEach { attachment ->
val id = attachment.getLong("Id") ?: return@forEach
val itemId = attachment.getLong(idName) ?: return@forEach
val url = attachment.getString("Url") ?: return@forEach
val fileName = "${attachment.getString("NazwaPliku")}:$url"
list.forEach {
if (it is MessageFull
&& it.profileId == profileId
&& it.id == itemId
&& it.attachmentIds?.contains(id) != true) {
if (it.attachmentIds == null)
it.attachmentIds = mutableListOf()
if (it.attachmentNames == null)
it.attachmentNames = mutableListOf()
it.attachmentIds?.add(id)
it.attachmentNames?.add(fileName)
}
if (it is EventFull
&& it.profileId == profileId
&& it.id == itemId
&& it.attachmentIds?.contains(id) != true) {
if (it.attachmentIds == null)
it.attachmentIds = mutableListOf()
if (it.attachmentNames == null)
it.attachmentNames = mutableListOf()
it.attachmentIds?.add(id)
it.attachmentNames?.add(fileName)
}
if (owner is MessageFull
&& it is MessageFull
&& owner.profileId == it.profileId
&& owner.id == it.id) {
owner.attachmentIds = it.attachmentIds
owner.attachmentNames = it.attachmentNames
}
if (owner is EventFull
&& it is EventFull
&& owner.profileId == it.profileId
&& owner.id == it.id) {
owner.attachmentIds = it.attachmentIds
owner.attachmentNames = it.attachmentNames
}
}
}
/*if (owner is MessageFull) {
list.forEach {
(it as? MessageFull)?.let { message ->
data.messageList.add(message)
}
}
data.messageListReplace = true
}
if (owner is EventFull) {
list.forEach {
(it as? EventFull)?.let { it1 ->
it1.homeworkBody = ""
data.eventList.add(it1)
}
}
data.eventListReplace = true
}*/
onSuccess(list)
}
}}
}

View File

@ -197,6 +197,13 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
profile.userCode = generateUserCode() profile.userCode = generateUserCode()
// update profile subname with class name, school year and account type
profile.subname = joinNotNullStrings(
" - ",
profile.studentClassName,
"${profile.studentSchoolYearStart}/${profile.studentSchoolYearStart + 1}"
) + " " + app.getString(if (profile.isParent) R.string.login_summary_account_parent else R.string.login_summary_account_child)
db.profileDao().add(profile) db.profileDao().add(profile)
db.loginStoreDao().add(loginStore) db.loginStoreDao().add(loginStore)

View File

@ -46,6 +46,6 @@ object Signing {
/*fun provideKey(param1: String, param2: Long): ByteArray {*/ /*fun provideKey(param1: String, param2: Long): ByteArray {*/
fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { fun pleaseStopRightNow(param1: String, param2: Long): ByteArray {
return "$param1.MTIzNDU2Nzg5MD43hCWBBS===.$param2".sha256() return "$param1.MTIzNDU2Nzg5MDP/4SAI6B===.$param2".sha256()
} }
} }

View File

@ -17,6 +17,7 @@ import com.google.gson.JsonObject
import pl.droidsonroids.gif.GifDrawable import pl.droidsonroids.gif.GifDrawable
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_EDUDZIENNIK import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_EDUDZIENNIK
import pl.szczodrzynski.edziennik.utils.ProfileImageHolder
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.ImageHolder import pl.szczodrzynski.navlib.ImageHolder
import pl.szczodrzynski.navlib.R import pl.szczodrzynski.navlib.R
@ -128,7 +129,7 @@ open class Profile(
override fun getImageHolder(context: Context): ImageHolder { override fun getImageHolder(context: Context): ImageHolder {
return if (!image.isNullOrEmpty()) { return if (!image.isNullOrEmpty()) {
try { try {
ImageHolder(image ?: "") ProfileImageHolder(image ?: "")
} catch (_: Exception) { } catch (_: Exception) {
ImageHolder(R.drawable.profile, colorFromName(name)) ImageHolder(R.drawable.profile, colorFromName(name))
} }

View File

@ -244,6 +244,8 @@ class EventDetailsDialog(
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true) @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onEventGetEvent(event: EventGetEvent) { fun onEventGetEvent(event: EventGetEvent) {
EventBus.getDefault().removeStickyEvent(event) EventBus.getDefault().removeStickyEvent(event)
if (event.event.homeworkBody == null)
event.event.homeworkBody = ""
update() update()
} }

View File

@ -60,5 +60,9 @@ class LabFragment : Fragment(), CoroutineScope {
b.rodo.onClick { b.rodo.onClick {
app.db.teacherDao().query(SimpleSQLiteQuery("UPDATE teachers SET teacherSurname = \"\" WHERE profileId = ${App.profileId}")) app.db.teacherDao().query(SimpleSQLiteQuery("UPDATE teachers SET teacherSurname = \"\" WHERE profileId = ${App.profileId}"))
} }
b.removeHomework.onClick {
app.db.eventDao().getRawNow("UPDATE events SET homeworkBody = NULL WHERE profileId = ${App.profileId}")
}
} }
} }

View File

@ -112,6 +112,9 @@ class SubjectViewHolder(
)) ))
} }
// remove previously added grades from year preview
if (b.yearContainer.childCount > 1)
b.yearContainer.removeViews(1, b.yearContainer.childCount - 1)
// add the yearly grades to summary container (expanded) // add the yearly grades to summary container (expanded)
item.proposedGrade?.let { item.proposedGrade?.let {
b.yearContainer.addView(GradeView( b.yearContainer.addView(GradeView(

View File

@ -85,6 +85,7 @@ class LoginChooserFragment : Fragment() {
} }
b.devMode.visibility = if (App.debugMode) View.VISIBLE else View.GONE b.devMode.visibility = if (App.debugMode) View.VISIBLE else View.GONE
b.devMode.isChecked = app.config.debugMode
b.devMode.onChange { v, isChecked -> b.devMode.onChange { v, isChecked ->
if (isChecked) { if (isChecked) {
MaterialDialog.Builder(activity) MaterialDialog.Builder(activity)
@ -94,6 +95,7 @@ class LoginChooserFragment : Fragment() {
.negativeText(R.string.no) .negativeText(R.string.no)
.onPositive { _: MaterialDialog?, _: DialogAction? -> .onPositive { _: MaterialDialog?, _: DialogAction? ->
app.config.debugMode = true app.config.debugMode = true
App.devMode = true
MaterialAlertDialogBuilder(activity) MaterialAlertDialogBuilder(activity)
.setTitle("Restart") .setTitle("Restart")
.setMessage("Wymagany restart aplikacji") .setMessage("Wymagany restart aplikacji")
@ -104,12 +106,10 @@ class LoginChooserFragment : Fragment() {
} }
.setCancelable(false) .setCancelable(false)
.show() .show()
/*if (b.devModeLayout.getVisibility() !== View.VISIBLE) {
Anim.expand(b.devModeTitle, 500, null)
Anim.expand(b.devModeLayout, 500, null)
}*/
} }
.onNegative { _: MaterialDialog?, _: DialogAction? -> .onNegative { _: MaterialDialog?, _: DialogAction? ->
app.config.debugMode = false
App.devMode = false
b.devMode.isChecked = app.config.debugMode b.devMode.isChecked = app.config.debugMode
b.devMode.jumpDrawablesToCurrentState() b.devMode.jumpDrawablesToCurrentState()
Anim.collapse(b.devMode, 1000, null) Anim.collapse(b.devMode, 1000, null)
@ -117,6 +117,7 @@ class LoginChooserFragment : Fragment() {
.show() .show()
} else { } else {
app.config.debugMode = false app.config.debugMode = false
App.devMode = false
/*if (b.devModeLayout.getVisibility() === View.VISIBLE) { /*if (b.devModeLayout.getVisibility() === View.VISIBLE) {
Anim.collapse(b.devModeTitle, 500, null) Anim.collapse(b.devModeTitle, 500, null)
Anim.collapse(b.devModeLayout, 500, null) Anim.collapse(b.devModeLayout, 500, null)

View File

@ -108,6 +108,10 @@ class MessageFragment : Fragment(), CoroutineScope {
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show() .show()
} }
b.downloadButton.isVisible = App.debugMode
b.downloadButton.onClick {
EdziennikTask.messageGet(App.profileId, message).enqueue(activity)
}
launch { launch {
@ -182,14 +186,16 @@ class MessageFragment : Fragment(), CoroutineScope {
} }
} }
val readByAll = checkRecipients()
// if a sent msg is not read by everyone, download it again to check the read status if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_VULCAN) {
if (!checkRecipients() && app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN) { // vulcan: change message status or download attachments
EdziennikTask.messageGet(App.profileId, message).enqueue(activity) if (message.type == TYPE_RECEIVED && !message.seen || message.attachmentIds == null) {
return EdziennikTask.messageGet(App.profileId, message).enqueue(activity)
return
}
} }
else if (!readByAll) {
if(message.type == TYPE_RECEIVED && !message.seen && app.profile.loginStoreType == LoginStore.LOGIN_TYPE_VULCAN) { // if a sent msg is not read by everyone, download it again to check the read status
EdziennikTask.messageGet(App.profileId, message).enqueue(activity) EdziennikTask.messageGet(App.profileId, message).enqueue(activity)
return return
} }

View File

@ -55,6 +55,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog;
import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog; import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog;
import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog; import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog;
import pl.szczodrzynski.edziennik.ui.dialogs.sync.NotificationFilterDialog; import pl.szczodrzynski.edziennik.ui.dialogs.sync.NotificationFilterDialog;
import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity;
import pl.szczodrzynski.edziennik.utils.Themes; import pl.szczodrzynski.edziennik.utils.Themes;
import pl.szczodrzynski.edziennik.utils.Utils; import pl.szczodrzynski.edziennik.utils.Utils;
import pl.szczodrzynski.edziennik.utils.models.Date; import pl.szczodrzynski.edziennik.utils.models.Date;
@ -161,7 +162,7 @@ public class SettingsNewFragment extends MaterialAboutFragment {
profileCardTitleItem = new MaterialAboutProfileItem( profileCardTitleItem = new MaterialAboutProfileItem(
app.getProfile().getName(), app.getProfile().getName(),
getString(R.string.settings_profile_subtitle_format, app.getProfile().getSubname()), app.getProfile().getSubname(),
getProfileDrawable() getProfileDrawable()
); );
profileCardTitleItem.setOnClickAction(() -> { profileCardTitleItem.setOnClickAction(() -> {
@ -218,6 +219,20 @@ public class SettingsNewFragment extends MaterialAboutFragment {
}) })
);*/ );*/
items.add(
new MaterialAboutActionItem(
getString(R.string.settings_add_student_text),
getString(R.string.settings_add_student_subtext),
new IconicsDrawable(activity)
.icon(CommunityMaterial.Icon.cmd_account_plus_outline)
.size(IconicsSize.dp(iconSizeDp))
.color(IconicsColor.colorInt(iconColor))
)
.setOnClickAction(() -> {
startActivity(new Intent(activity, LoginActivity.class));
})
);
items.add( items.add(
new MaterialAboutActionItem( new MaterialAboutActionItem(
getString(R.string.settings_profile_notifications_text), getString(R.string.settings_profile_notifications_text),
@ -232,6 +247,20 @@ public class SettingsNewFragment extends MaterialAboutFragment {
}) })
); );
items.add(
new MaterialAboutActionItem(
getString(R.string.settings_profile_remove_text),
getString(R.string.settings_profile_remove_subtext),
new IconicsDrawable(activity)
.icon(SzkolnyFont.Icon.szf_delete_empty_outline)
.size(IconicsSize.dp(iconSizeDp))
.color(IconicsColor.colorInt(iconColor))
)
.setOnClickAction(() -> {
new ProfileRemoveDialog(activity, app.getProfile().getId(), app.getProfile().getName());
})
);
items.add(getMoreItem(() -> addCardItems(CARD_PROFILE, getProfileCard(true)))); items.add(getMoreItem(() -> addCardItems(CARD_PROFILE, getProfileCard(true))));
} }
else { else {
@ -253,20 +282,6 @@ public class SettingsNewFragment extends MaterialAboutFragment {
})) }))
); );
items.add(
new MaterialAboutActionItem(
getString(R.string.settings_profile_remove_text),
getString(R.string.settings_profile_remove_subtext),
new IconicsDrawable(activity)
.icon(SzkolnyFont.Icon.szf_delete_empty_outline)
.size(IconicsSize.dp(iconSizeDp))
.color(IconicsColor.colorInt(iconColor))
)
.setOnClickAction(() -> {
new ProfileRemoveDialog(activity, app.getProfile().getId(), app.getProfile().getName());
})
);
} }
return items; return items;
} }

View File

@ -58,8 +58,9 @@ class AttachmentAdapter(
val item = items[position] val item = items[position]
val b = holder.b val b = holder.b
val fileName = item.name.substringBefore(":http")
// create an icon for the attachment // create an icon for the attachment
val icon: IIcon = when (Utils.getExtensionFromFileName(item.name)) { val icon: IIcon = when (Utils.getExtensionFromFileName(fileName)) {
"doc", "docx", "odt", "rtf" -> SzkolnyFont.Icon.szf_file_word_outline "doc", "docx", "odt", "rtf" -> SzkolnyFont.Icon.szf_file_word_outline
"xls", "xlsx", "ods" -> SzkolnyFont.Icon.szf_file_excel_outline "xls", "xlsx", "ods" -> SzkolnyFont.Icon.szf_file_excel_outline
"ppt", "pptx", "odp" -> SzkolnyFont.Icon.szf_file_powerpoint_outline "ppt", "pptx", "odp" -> SzkolnyFont.Icon.szf_file_powerpoint_outline
@ -73,12 +74,12 @@ class AttachmentAdapter(
} }
b.chip.text = if (item.isDownloading) { b.chip.text = if (item.isDownloading) {
app.getString(R.string.messages_attachment_downloading_format, item.name, item.downloadProgress) app.getString(R.string.messages_attachment_downloading_format, fileName, item.downloadProgress)
} }
else { else {
item.size?.let { item.size?.let {
app.getString(R.string.messages_attachment_format, item.name, Utils.readableFileSize(it)) app.getString(R.string.messages_attachment_format, fileName, Utils.readableFileSize(it))
} ?: item.name } ?: fileName
} }
b.chip.chipIcon = IconicsDrawable(context) b.chip.chipIcon = IconicsDrawable(context)

View File

@ -8,12 +8,14 @@ import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.util.AttributeSet import android.util.AttributeSet
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent
@ -41,6 +43,8 @@ class AttachmentsView @JvmOverloads constructor(
} }
fun init(arguments: Bundle, owner: Any) { fun init(arguments: Bundle, owner: Any) {
val app = context.applicationContext as App
val activity = context as? AppCompatActivity ?: return
val list = this as? RecyclerView ?: return val list = this as? RecyclerView ?: return
val profileId = arguments.get<Int>("profileId") ?: return val profileId = arguments.get<Int>("profileId") ?: return
@ -49,12 +53,16 @@ class AttachmentsView @JvmOverloads constructor(
val attachmentSizes = arguments.getLongArray("attachmentSizes") val attachmentSizes = arguments.getLongArray("attachmentSizes")
val adapter = AttachmentAdapter(context, onAttachmentClick = { item -> val adapter = AttachmentAdapter(context, onAttachmentClick = { item ->
downloadAttachment(item) app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) {
downloadAttachment(item)
}
}, onAttachmentLongClick = { chip, item -> }, onAttachmentLongClick = { chip, item ->
val popupMenu = PopupMenu(chip.context, chip) val popupMenu = PopupMenu(chip.context, chip)
popupMenu.menu.add(0, 1, 0, R.string.messages_attachment_download_again) popupMenu.menu.add(0, 1, 0, R.string.messages_attachment_download_again)
popupMenu.setOnMenuItemClickListener { popupMenu.setOnMenuItemClickListener {
downloadAttachment(item, forceDownload = true) app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) {
downloadAttachment(item, forceDownload = true)
}
true true
} }
popupMenu.show() popupMenu.show()
@ -86,7 +94,20 @@ class AttachmentsView @JvmOverloads constructor(
try { try {
val attachmentFileName = Utils.getStringFromFile(attachmentDataFile) val attachmentFileName = Utils.getStringFromFile(attachmentDataFile)
val attachmentFile = File(attachmentFileName) val attachmentFile = File(attachmentFileName)
attachmentFile.exists() // get the correct file name and update
if (attachmentFile.exists()) {
// get the download url before updating file name
val fileUrl = item.name.substringAfter(":", missingDelimiterValue = "")
// update file name with the downloaded one
item.name = attachmentFile.name
// save the download url back
if (fileUrl != "")
item.name += ":$fileUrl"
true
}
else false
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
false false
@ -97,7 +118,8 @@ class AttachmentsView @JvmOverloads constructor(
private fun downloadAttachment(attachment: AttachmentAdapter.Item, forceDownload: Boolean = false) { private fun downloadAttachment(attachment: AttachmentAdapter.Item, forceDownload: Boolean = false) {
if (!forceDownload && attachment.isDownloaded) { if (!forceDownload && attachment.isDownloaded) {
Utils.openFile(context, File(Utils.getStorageDir(), attachment.name)) // open file by name, or first part before ':' (Vulcan OneDrive)
Utils.openFile(context, File(Utils.getStorageDir(), attachment.name.substringBefore(":")))
return return
} }
@ -128,17 +150,19 @@ class AttachmentsView @JvmOverloads constructor(
when (event.eventType) { when (event.eventType) {
AttachmentGetEvent.TYPE_FINISHED -> { AttachmentGetEvent.TYPE_FINISHED -> {
// save the downloaded file name // save the downloaded file name
attachment.downloadedName = event.fileName
attachment.isDownloading = false attachment.isDownloading = false
attachment.isDownloaded = true attachment.isDownloaded = true
// update file name for iDziennik which // get the download url before updating file name
// does not provide the name before downloading val fileUrl = attachment.name.substringAfter(":", missingDelimiterValue = "")
if (!attachment.name.contains(".")) // update file name with the downloaded one
attachment.name = File(attachment.downloadedName).name attachment.name = File(event.fileName).name
// save the download url back
if (fileUrl != "")
attachment.name += ":$fileUrl"
// open the file // open the file
Utils.openFile(context, File(Utils.getStorageDir(), attachment.name)) Utils.openFile(context, File(event.fileName))
} }
AttachmentGetEvent.TYPE_PROGRESS -> { AttachmentGetEvent.TYPE_PROGRESS -> {

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-7.
*/
package pl.szczodrzynski.edziennik.utils
import android.widget.ImageView
import pl.szczodrzynski.navlib.ImageHolder
class ProfileImageHolder(url: String) : ImageHolder(url) {
override fun applyTo(imageView: ImageView, tag: String?): Boolean {
return try {
super.applyTo(imageView, tag)
} catch (_: Exception) { false }
}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-7.
*/
package pl.szczodrzynski.edziennik.utils.managers
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.qifan.powerpermission.coroutines.awaitAskPermissions
import com.qifan.powerpermission.data.hasAllGranted
import com.qifan.powerpermission.data.hasPermanentDenied
import com.qifan.powerpermission.data.hasRational
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import kotlin.coroutines.CoroutineContext
class PermissionManager(val app: App) : CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private fun isStoragePermissionGranted() = if (Build.VERSION.SDK_INT >= 23) {
app.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
} else {
true
}
fun requestStoragePermission(
activity: AppCompatActivity,
@StringRes permissionMessage: Int,
onSuccess: suspend CoroutineScope.() -> Unit
) {
launch {
if (isStoragePermissionGranted()) {
onSuccess()
return@launch
}
val result = activity.awaitAskPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
when {
result.hasAllGranted() -> onSuccess()
result.hasRational() -> {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.permissions_required)
.setMessage(permissionMessage)
.setPositiveButton(R.string.ok) { _, _ ->
requestStoragePermission(activity, permissionMessage, onSuccess)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
result.hasPermanentDenied() -> {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.permissions_required)
.setMessage(R.string.permissions_denied)
.setPositiveButton(R.string.ok) { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", app.packageName, null)
intent.data = uri
activity.startActivity(intent)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
}
}
}

View File

@ -47,6 +47,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Duh rodo button" android:text="Duh rodo button"
android:textAllCaps="false" /> android:textAllCaps="false" />
<Button
android:id="@+id/removeHomework"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Remove all homework body (null)"
android:textAllCaps="false" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
</layout> </layout>

View File

@ -295,6 +295,39 @@
android:textAppearance="@style/NavView.TextView.Small" /> android:textAppearance="@style/NavView.TextView.Small" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/downloadButton"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_weight="1"
android:background="@drawable/bg_rounded_ripple"
android:gravity="center"
android:orientation="vertical"
android:paddingLeft="4dp"
android:paddingTop="8dp"
android:paddingRight="4dp"
android:paddingBottom="8dp"
android:visibility="visible">
<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-download-outline"
tools:srcCompat="@android:drawable/ic_menu_delete" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Pobierz ponownie"
android:textAppearance="@style/NavView.TextView.Small" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

File diff suppressed because it is too large Load Diff

View File

@ -128,6 +128,7 @@
<string name="error_340" translatable="false">ERROR_VULCAN_API_MAINTENANCE</string> <string name="error_340" translatable="false">ERROR_VULCAN_API_MAINTENANCE</string>
<string name="error_341" translatable="false">ERROR_VULCAN_API_BAD_REQUEST</string> <string name="error_341" translatable="false">ERROR_VULCAN_API_BAD_REQUEST</string>
<string name="error_342" translatable="false">ERROR_VULCAN_API_OTHER</string> <string name="error_342" translatable="false">ERROR_VULCAN_API_OTHER</string>
<string name="error_343" translatable="false">ERROR_VULCAN_ATTACHMENT_DOWNLOAD</string>
<string name="error_401" translatable="false">ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN</string> <string name="error_401" translatable="false">ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN</string>
<string name="error_402" translatable="false">ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME</string> <string name="error_402" translatable="false">ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME</string>
@ -177,6 +178,7 @@
<string name="error_914" translatable="false">EXCEPTION_IDZIENNIK_API_REQUEST</string> <string name="error_914" translatable="false">EXCEPTION_IDZIENNIK_API_REQUEST</string>
<string name="error_920" translatable="false">EXCEPTION_EDUDZIENNIK_WEB_REQUEST</string> <string name="error_920" translatable="false">EXCEPTION_EDUDZIENNIK_WEB_REQUEST</string>
<string name="error_921" translatable="false">EXCEPTION_EDUDZIENNIK_FILE_REQUEST</string> <string name="error_921" translatable="false">EXCEPTION_EDUDZIENNIK_FILE_REQUEST</string>
<string name="error_930" translatable="false">ERROR_ONEDRIVE_DOWNLOAD</string>
<string name="error_1201" translatable="false">LOGIN_NO_ARGUMENTS</string> <string name="error_1201" translatable="false">LOGIN_NO_ARGUMENTS</string>
@ -246,7 +248,7 @@
<string name="error_157_reason">Brak ID sesji Wiadomości</string> <string name="error_157_reason">Brak ID sesji Wiadomości</string>
<string name="error_158_reason">Odmowa dostępu do Portalu Librus</string> <string name="error_158_reason">Odmowa dostępu do Portalu Librus</string>
<string name="error_159_reason">API Portalu Librus wyłączone</string> <string name="error_159_reason">API Portalu Librus wyłączone</string>
<string name="error_160_reason">Konto Synergia zostało rozłączone</string> <string name="error_160_reason">Konto LIBRUS utraciło połączenie z kontem Synergia. Zaloguj się na stronie portal.librus.pl lub w oficjalnej aplikacji Librus i postępuj zgodnie z instrukcją, aby naprawić konto.</string>
<string name="error_161_reason">Inny błąd Portalu Librus</string> <string name="error_161_reason">Inny błąd Portalu Librus</string>
<string name="error_162_reason">Nie znaleziono konta Synergia. Zaloguj się na stronie portal.librus.pl, a następnie powiąż swoje konto Synergia do konta Librus Portal.</string> <string name="error_162_reason">Nie znaleziono konta Synergia. Zaloguj się na stronie portal.librus.pl, a następnie powiąż swoje konto Synergia do konta Librus Portal.</string>
<string name="error_163_reason">Inny błąd logowania do Portalu Librus</string> <string name="error_163_reason">Inny błąd logowania do Portalu Librus</string>
@ -304,6 +306,7 @@
<string name="error_340_reason">Vulcan: przerwa techniczna</string> <string name="error_340_reason">Vulcan: przerwa techniczna</string>
<string name="error_341_reason">Vulcan: błąd żądania, zgłoś błąd</string> <string name="error_341_reason">Vulcan: błąd żądania, zgłoś błąd</string>
<string name="error_342_reason">Vulcan: inny błąd, wyślij zgłoszenie</string> <string name="error_342_reason">Vulcan: inny błąd, wyślij zgłoszenie</string>
<string name="error_343_reason">Vulcan: nie znaleziono adresu załącznika</string>
<string name="error_401_reason">Nieprawidłowe dane logowania</string> <string name="error_401_reason">Nieprawidłowe dane logowania</string>
<string name="error_402_reason">Nieprawidłowa nazwa szkoły</string> <string name="error_402_reason">Nieprawidłowa nazwa szkoły</string>
@ -353,6 +356,7 @@
<string name="error_914_reason">EXCEPTION_IDZIENNIK_API_REQUEST</string> <string name="error_914_reason">EXCEPTION_IDZIENNIK_API_REQUEST</string>
<string name="error_920_reason">Wystąpił błąd</string> <string name="error_920_reason">Wystąpił błąd</string>
<string name="error_921_reason">Wystąpił błąd podczas pobierania pliku</string> <string name="error_921_reason">Wystąpił błąd podczas pobierania pliku</string>
<string name="error_930_reason">Nie udało się pobrać pliku z OneDrive</string>
<string name="error_1201_reason">Nie podano parametrów</string> <string name="error_1201_reason">Nie podano parametrów</string>
</resources> </resources>

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,8 @@ buildscript {
kotlin_version = '1.3.61' kotlin_version = '1.3.61'
release = [ release = [
versionName: "4.0-rc.5", versionName: "4.0",
versionCode: 4000059 versionCode: 4000099
] ]
setup = [ setup = [
@ -87,6 +87,7 @@ allprojects {
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
maven { url "https://kotlin.bintray.com/kotlinx/" } maven { url "https://kotlin.bintray.com/kotlinx/" }
maven { url "https://dl.bintray.com/wulkanowy/wulkanowy" } maven { url "https://dl.bintray.com/wulkanowy/wulkanowy" }
maven { url "https://dl.bintray.com/undervoid/PowerPermission" }
} }
} }

View File

@ -6,6 +6,7 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URLDecoder;
import im.wangchao.mhttp.AbsCallbackHandler; import im.wangchao.mhttp.AbsCallbackHandler;
import im.wangchao.mhttp.Accept; import im.wangchao.mhttp.Accept;
@ -69,7 +70,11 @@ public class FileCallbackHandler extends AbsCallbackHandler<File> {
if (this.file.isDirectory()) { if (this.file.isDirectory()) {
String contentDisposition = response.header("content-disposition"); String contentDisposition = response.header("content-disposition");
if (contentDisposition != null) { if (contentDisposition != null) {
String filename = contentDisposition.substring(contentDisposition.indexOf("\"")+1, contentDisposition.lastIndexOf("\"")); if (contentDisposition.contains("*=UTF-8")) {
contentDisposition = contentDisposition.replace("*=UTF-8''", "\"") + "\"";
contentDisposition = URLDecoder.decode(contentDisposition, "UTF-8");
}
String filename = contentDisposition.substring(contentDisposition.indexOf("\"") + 1, contentDisposition.lastIndexOf("\""));
this.file = new File(file, filename); this.file = new File(file, filename);
file = this.file; file = this.file;
} }