[API] Add Vulcan OneDrive attachment downloading. Add asking for permissions on demand.

This commit is contained in:
Kuba Szczodrzyński 2020-04-07 12:16:48 +02:00
parent 0327ba37f1
commit d56afb034b
19 changed files with 320 additions and 38 deletions

View File

@ -41,5 +41,15 @@
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</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>
</project>

View File

@ -198,6 +198,9 @@ dependencies {
kapt project(":codegen")
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 {
mavenCentral()

View File

@ -14,6 +14,9 @@
<uses-permission android:name="android.permission.CAMERA" />
<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
android:name=".App"
android:allowBackup="true"

View File

@ -65,6 +65,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val gradesManager by lazy { GradesManager(this) }
val timetableManager by lazy { TimetableManager(this) }
val eventManager by lazy { EventManager(this) }
val permissionManager by lazy { PermissionManager(this) }
val db
get() = App.db

View File

@ -414,8 +414,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
R.color.md_green_500
)
isStoragePermissionGranted()
SyncWorker.scheduleNext(app)
UpdateWorker.scheduleNext(app)

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_BAD_REQUEST = 341
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_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_EDUDZIENNIK_WEB_REQUEST = 920
const val EXCEPTION_EDUDZIENNIK_FILE_REQUEST = 921
const val ERROR_ONEDRIVE_DOWNLOAD = 930
const val LOGIN_NO_ARGUMENTS = 1201

View File

@ -0,0 +1,103 @@
/*
* 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")
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("&", "?")
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) {
sandboxGet(LibrusMessagesGetAttachment.TAG, "CSCheckKey",
sandboxGet(TAG, "CSCheckKey",
parameters = mapOf("singleUseKey" to attachmentKey)) { json ->
when (json.getString("status")) {
"not_downloaded_yet" -> {
if (getAttachmentCheckKeyTries++ > 5) {
data.error(ApiError(LibrusMessagesGetAttachment.TAG, ERROR_FILE_DOWNLOAD)
data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD)
.withApiResponse(json))
return@sandboxGet
}
@ -75,7 +75,7 @@ class LibrusSandboxDownloadAttachment(override val data: DataLibrus,
}
else -> {
data.error(ApiError(LibrusMessagesGetAttachment.TAG, EXCEPTION_LIBRUS_MESSAGES_REQUEST)
data.error(ApiError(TAG, EXCEPTION_LIBRUS_MESSAGES_REQUEST)
.withApiResponse(json))
}
}
@ -85,7 +85,7 @@ class LibrusSandboxDownloadAttachment(override val data: DataLibrus,
private fun downloadAttachment(url: String, method: Int = GET) {
val targetFile = File(Utils.getStorageDir(), attachmentName)
sandboxGetFile(LibrusMessagesGetAttachment.TAG, url, targetFile, { file ->
sandboxGetFile(TAG, url, targetFile, { file ->
val event = AttachmentGetEvent(
profileId,

View File

@ -7,28 +7,29 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan
import com.google.gson.JsonObject
import org.greenrobot.eventbus.EventBus
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.api.VulcanApiAttachments
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.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.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.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.utils.Utils
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 {
companion object {
@ -128,8 +129,51 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
override fun markAllAnnouncementsAsRead() {}
override fun getAnnouncement(announcement: AnnouncementFull) {}
override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) {}
override fun getRecipientList() {}
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 }

View File

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

View File

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

View File

@ -58,8 +58,9 @@ class AttachmentAdapter(
val item = items[position]
val b = holder.b
val fileName = item.name.substringBefore(":http")
// 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
"xls", "xlsx", "ods" -> SzkolnyFont.Icon.szf_file_excel_outline
"ppt", "pptx", "odp" -> SzkolnyFont.Icon.szf_file_powerpoint_outline
@ -73,12 +74,12 @@ class AttachmentAdapter(
}
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 {
item.size?.let {
app.getString(R.string.messages_attachment_format, item.name, Utils.readableFileSize(it))
} ?: item.name
app.getString(R.string.messages_attachment_format, fileName, Utils.readableFileSize(it))
} ?: fileName
}
b.chip.chipIcon = IconicsDrawable(context)

View File

@ -8,12 +8,14 @@ import android.content.Context
import android.os.Bundle
import android.os.Environment
import android.util.AttributeSet
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent
@ -41,6 +43,8 @@ class AttachmentsView @JvmOverloads constructor(
}
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 profileId = arguments.get<Int>("profileId") ?: return
@ -49,12 +53,16 @@ class AttachmentsView @JvmOverloads constructor(
val attachmentSizes = arguments.getLongArray("attachmentSizes")
val adapter = AttachmentAdapter(context, onAttachmentClick = { item ->
app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) {
downloadAttachment(item)
}
}, onAttachmentLongClick = { chip, item ->
val popupMenu = PopupMenu(chip.context, chip)
popupMenu.menu.add(0, 1, 0, R.string.messages_attachment_download_again)
popupMenu.setOnMenuItemClickListener {
app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) {
downloadAttachment(item, forceDownload = true)
}
true
}
popupMenu.show()
@ -97,7 +105,8 @@ class AttachmentsView @JvmOverloads constructor(
private fun downloadAttachment(attachment: AttachmentAdapter.Item, forceDownload: Boolean = false) {
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
}
@ -128,17 +137,19 @@ class AttachmentsView @JvmOverloads constructor(
when (event.eventType) {
AttachmentGetEvent.TYPE_FINISHED -> {
// save the downloaded file name
attachment.downloadedName = event.fileName
attachment.isDownloading = false
attachment.isDownloaded = true
// update file name for iDziennik which
// does not provide the name before downloading
if (!attachment.name.contains("."))
attachment.name = File(attachment.downloadedName).name
// get the download url before updating file name
val fileUrl = attachment.name.substringBefore(":", missingDelimiterValue = "")
// update file name with the downloaded one
attachment.name = File(event.fileName).name
// save the download url back
if (fileUrl != "")
attachment.name += ":$fileUrl"
// open the file
Utils.openFile(context, File(Utils.getStorageDir(), attachment.name))
Utils.openFile(context, File(event.fileName))
}
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

@ -88,7 +88,7 @@
<string name="day_today_format">today (%s)</string>
<string name="day_tomorrow_format">tomorrow (%s)</string>
<string name="debug_notice">You shouldn\'t really care about what you see here.</string>
<string name="dev_mode_enable_warning">These settings are intended for this app\'s developers, not for normal users.\n\nThey are not described in any way, so using them may lead to breaking some functions in the app, damaging your system or losing data.\n\nBe careful and use them wisely.</string>
<string name="dev_mode_enable_warning">These settings are intended for this app\'s developers, not for normal users.\n\nThey are not described in any way, so using them may lead to breaking some functions in the app, damaging your system, losing data or even installing a virus on the battery.\n\nBe careful and use them wisely.</string>
<string name="developer_mode">Developer mode</string>
<string name="dialog_averages_expected_format">Predicted semester %d average:\n%#.2f\n</string>
<string name="dialog_averages_expected_yearly_format">Predicted yearly average:\n%#.2f\n</string>
@ -431,7 +431,7 @@
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="help">Help</string>
<string name="help_notification_web_push">Notification forwarding allows you to pair a PC web browser to receive notifications on your desktop. This includes new grades, events, homework etc.\n\nClick \"Notification forwarding\" to begin.</string>
<string name="help_register_agreement">Registraton will run automatically on the first login.\n\nThere will be some data sent to the app server:\n- your school and class ID\n- your e-register username\n- your first and last name\n\nThe only data visible to others is your name (when sharing events). Any private data (like password, grades etc.) won\'t be sent anywhere. Learn more in the Privacy policy.</string>
<string name="help_register_agreement">Registration will run automatically on the first login.\n\nThere will be some data sent to the app server:\n- your school and class ID\n- your e-register username\n- your first and last name\n\nThe only data visible to others is your name (when sharing events). Any private data (like password, grades etc.) won\'t be sent anywhere. Learn more in the Privacy policy.</string>
<string name="hint_download_again">Download again</string>
<string name="hint_edit_event">Edit event</string>
<string name="hint_go_to_timetable">Go to timetable</string>
@ -683,9 +683,9 @@
<string name="messages_draft_title">Draft</string>
<string name="messages_no_data">You don\'t have any messages</string>
<string name="messages_recipient_list_download_error">Error getting the recipient list</string>
<string name="messages_recipients_list_read_format"><![CDATA[<li>&amp;nbsp;%s, read: %s, %s</li>]]></string>
<string name="messages_recipients_list_read_unknown_date_format"><![CDATA[<li>&amp;nbsp;%s, read: yes</li>]]></string>
<string name="messages_recipients_list_unread_format"><![CDATA[<li>&amp;nbsp;%s, read: <font color=red>no</font></li>]]></string>
<string name="messages_recipients_list_read_format"><![CDATA[<li>&nbsp;%s, read: %s, %s</li>]]></string>
<string name="messages_recipients_list_read_unknown_date_format"><![CDATA[<li>&nbsp;%s, read: yes</li>]]></string>
<string name="messages_recipients_list_unread_format"><![CDATA[<li>&nbsp;%s, read: <font color=red>no</font></li>]]></string>
<string name="messages_reply">Reply</string>
<string name="messages_reply_date_time_format">%s at %s</string>
<string name="messages_search">Search</string>

View File

@ -128,6 +128,7 @@
<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_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_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_920" translatable="false">EXCEPTION_EDUDZIENNIK_WEB_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>
@ -304,6 +306,7 @@
<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_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_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_920_reason">Wystąpił błąd</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>
</resources>

View File

@ -38,7 +38,7 @@
<string name="messages_attachment_format" translatable="false">%s (%s)</string>
<string name="messages_attachment_no_size_format" translatable="false">%s</string>
<string name="messages_date_time_format" translatable="false">%s, %s</string>
<string name="messages_recipients_list_unknown_state_format" translatable="false"><![CDATA[<li>&amp;nbsp;%s</li>]]></string>
<string name="messages_recipients_list_unknown_state_format" translatable="false"><![CDATA[<li>&nbsp;%s</li>]]></string>
<string name="notification_ticker_format" translatable="false">Szkolny.eu: %s</string>
<string name="preference_file" translatable="false">pl.szczodrzynski.edziennik_preferences</string>
<string name="preference_file_global" translatable="false">pl.szczodrzynski.edziennik_profiles</string>
@ -736,9 +736,9 @@
<string name="messages_draft_title">Wersja robocza</string>
<string name="messages_no_data">Nie masz żadnych wiadomości.</string>
<string name="messages_recipient_list_download_error">Błąd pobierania listy odbiorców</string>
<string name="messages_recipients_list_read_format"><![CDATA[<li>&amp;nbsp;%s, przeczytano: %s, %s</li>]]></string>
<string name="messages_recipients_list_read_unknown_date_format"><![CDATA[<li>&amp;nbsp;%s, przeczytano: tak</li>]]></string>
<string name="messages_recipients_list_unread_format"><![CDATA[<li>&amp;nbsp;%s, przeczytano: <font color=red>nie</font></li>]]></string>
<string name="messages_recipients_list_read_format"><![CDATA[<li>&nbsp;%s, przeczytano: %s, %s</li>]]></string>
<string name="messages_recipients_list_read_unknown_date_format"><![CDATA[<li>&nbsp;%s, przeczytano: tak</li>]]></string>
<string name="messages_recipients_list_unread_format"><![CDATA[<li>&nbsp;%s, przeczytano: <font color=red>nie</font></li>]]></string>
<string name="messages_reply">Odpowiedz</string>
<string name="messages_reply_date_time_format">%s o %s</string>
<string name="messages_search">Szukaj</string>
@ -1281,4 +1281,7 @@
<string name="yesterday">wczoraj</string>
<string name="you_are_offline_text">Jesteś offline. Spróbuj włączyć Wi-Fi lub dane komórkowe.</string>
<string name="you_are_offline_title">Połączenie sieciowe</string>
<string name="permissions_required">Wymagane uprawnienia</string>
<string name="permissions_denied">Odmówiłeś aplikacji wymaganych uprawnień do wykonania tej czynności.\n\nAby przynać uprawnienia, otwórz sekcję Uprawnienia dla aplikacji Szkolny.eu w ustawieniach telefonu.\n\nKliknij OK, aby przejść do ustawień aplikacji.</string>
<string name="permissions_attachment">Musisz przyznać uprawnienia do zapisu plików w pamięci telefonu, aby móc pobrać załącznik.\n\nKliknij OK, aby przyznać uprawnienia.</string>
</resources>

View File

@ -87,6 +87,7 @@ allprojects {
maven { url 'https://jitpack.io' }
maven { url "https://kotlin.bintray.com/kotlinx/" }
maven { url "https://dl.bintray.com/wulkanowy/wulkanowy" }
maven { url "https://dl.bintray.com/undervoid/PowerPermission" }
}
}