[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="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

@ -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

View File

@ -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

@ -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

@ -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) { 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

@ -7,28 +7,29 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan
import com.google.gson.JsonObject import com.google.gson.JsonObject
import org.greenrobot.eventbus.EventBus 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.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.EventGetEvent
import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent 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 {
@ -128,8 +129,51 @@ 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 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) { override fun getEvent(eventFull: EventFull) {
login(LOGIN_METHOD_VULCAN_API) { login(LOGIN_METHOD_VULCAN_API) {
val list = data.app.db.eventDao().getAllNow(data.profileId).filter { !it.addedManually } 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.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

@ -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

@ -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 ->
app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) {
downloadAttachment(item) 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 {
app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) {
downloadAttachment(item, forceDownload = true) downloadAttachment(item, forceDownload = true)
}
true true
} }
popupMenu.show() popupMenu.show()
@ -97,7 +105,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 +137,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.substringBefore(":", 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

@ -88,7 +88,7 @@
<string name="day_today_format">today (%s)</string> <string name="day_today_format">today (%s)</string>
<string name="day_tomorrow_format">tomorrow (%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="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="developer_mode">Developer mode</string>
<string name="dialog_averages_expected_format">Predicted semester %d average:\n%#.2f\n</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> <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="hello_blank_fragment">Hello blank fragment</string>
<string name="help">Help</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_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_download_again">Download again</string>
<string name="hint_edit_event">Edit event</string> <string name="hint_edit_event">Edit event</string>
<string name="hint_go_to_timetable">Go to timetable</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_draft_title">Draft</string>
<string name="messages_no_data">You don\'t have any messages</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_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_format"><![CDATA[<li>&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_read_unknown_date_format"><![CDATA[<li>&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_unread_format"><![CDATA[<li>&nbsp;%s, read: <font color=red>no</font></li>]]></string>
<string name="messages_reply">Reply</string> <string name="messages_reply">Reply</string>
<string name="messages_reply_date_time_format">%s at %s</string> <string name="messages_reply_date_time_format">%s at %s</string>
<string name="messages_search">Search</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_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>
@ -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>

View File

@ -38,7 +38,7 @@
<string name="messages_attachment_format" translatable="false">%s (%s)</string> <string name="messages_attachment_format" translatable="false">%s (%s)</string>
<string name="messages_attachment_no_size_format" translatable="false">%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_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="notification_ticker_format" translatable="false">Szkolny.eu: %s</string>
<string name="preference_file" translatable="false">pl.szczodrzynski.edziennik_preferences</string> <string name="preference_file" translatable="false">pl.szczodrzynski.edziennik_preferences</string>
<string name="preference_file_global" translatable="false">pl.szczodrzynski.edziennik_profiles</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_draft_title">Wersja robocza</string>
<string name="messages_no_data">Nie masz żadnych wiadomości.</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_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_format"><![CDATA[<li>&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_read_unknown_date_format"><![CDATA[<li>&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_unread_format"><![CDATA[<li>&nbsp;%s, przeczytano: <font color=red>nie</font></li>]]></string>
<string name="messages_reply">Odpowiedz</string> <string name="messages_reply">Odpowiedz</string>
<string name="messages_reply_date_time_format">%s o %s</string> <string name="messages_reply_date_time_format">%s o %s</string>
<string name="messages_search">Szukaj</string> <string name="messages_search">Szukaj</string>
@ -1281,4 +1281,7 @@
<string name="yesterday">wczoraj</string> <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_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="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> </resources>

View File

@ -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" }
} }
} }