mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-23 11:39:15 +00:00
RandomAccessCallback.Wrapper: support multiple state machine instances at same time (bitfireAT/davx5#428)
* RandomAccessCallback.Wrapper: support multiple state machine instances at same time - support multiple state machine instances at same time - provide explicit Exception/error code when the remote server doesn't support ranged requests * Only use RandomAccessCallback when server explicitly advertises range requests --------- Co-authored-by: Arnau Mora <arnyminerz@proton.me>
This commit is contained in:
parent
fbe0c4451b
commit
cf9340107f
|
@ -17,6 +17,7 @@ import android.graphics.Point
|
|||
import android.media.ThumbnailUtils
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.CancellationSignal
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.storage.StorageManager
|
||||
|
@ -468,7 +469,7 @@ class DavDocumentsProvider: DocumentsProvider() {
|
|||
}
|
||||
deferredFileInfo.get()
|
||||
}
|
||||
Logger.log.info("Received file info: $fileInfo")
|
||||
Logger.log.fine("Received file info: $fileInfo")
|
||||
|
||||
// RandomAccessCallback.Wrapper / StreamingFileDescriptor are responsible for closing httpClient
|
||||
return if (
|
||||
|
@ -476,11 +477,13 @@ class DavDocumentsProvider: DocumentsProvider() {
|
|||
readAccess && // WebDAV doesn't support random write access natively
|
||||
fileInfo.size != null && // file descriptor must return a useful value on getFileSize()
|
||||
(fileInfo.eTag != null || fileInfo.lastModified != null) && // we need a method to determine whether the document has changed during access
|
||||
fileInfo.supportsPartial != false // WebDAV server must support random access
|
||||
fileInfo.supportsPartial == true // WebDAV server must support random access
|
||||
) {
|
||||
Logger.log.fine("Creating RandomAccessCallback for $url")
|
||||
val accessor = RandomAccessCallback.Wrapper(ourContext, client, url, doc.mimeType, fileInfo, signal)
|
||||
storageManager.openProxyFileDescriptor(modeFlags, accessor, accessor.workerHandler)
|
||||
} else {
|
||||
Logger.log.fine("Creating StreamingFileDescriptor for $url")
|
||||
val fd = StreamingFileDescriptor(ourContext, client, url, doc.mimeType, signal) { transferred ->
|
||||
// called when transfer is finished
|
||||
|
||||
|
|
|
@ -33,18 +33,18 @@ import okhttp3.Headers
|
|||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType
|
||||
import org.apache.commons.io.FileUtils
|
||||
import ru.nsk.kstatemachine.DefaultState
|
||||
import ru.nsk.kstatemachine.Event
|
||||
import ru.nsk.kstatemachine.FinalState
|
||||
import ru.nsk.kstatemachine.State
|
||||
import ru.nsk.kstatemachine.StateMachine
|
||||
import ru.nsk.kstatemachine.addFinalState
|
||||
import ru.nsk.kstatemachine.addInitialState
|
||||
import ru.nsk.kstatemachine.createStdLibStateMachine
|
||||
import ru.nsk.kstatemachine.finalState
|
||||
import ru.nsk.kstatemachine.initialState
|
||||
import ru.nsk.kstatemachine.onEntry
|
||||
import ru.nsk.kstatemachine.onExit
|
||||
import ru.nsk.kstatemachine.onFinished
|
||||
import ru.nsk.kstatemachine.processEventBlocking
|
||||
import ru.nsk.kstatemachine.transition
|
||||
import ru.nsk.kstatemachine.state
|
||||
import ru.nsk.kstatemachine.transitionOn
|
||||
import java.io.InterruptedIOException
|
||||
import java.lang.ref.WeakReference
|
||||
import java.net.HttpURLConnection
|
||||
|
@ -77,8 +77,7 @@ class RandomAccessCallback private constructor(
|
|||
if (cache != null)
|
||||
return cache
|
||||
|
||||
Logger.log.info("Creating memory cache")
|
||||
|
||||
Logger.log.fine("Creating memory cache")
|
||||
val maxHeapSizeMB = context.getSystemService<ActivityManager>()!!.memoryClass
|
||||
val cacheSize = maxHeapSizeMB * FileUtils.ONE_MB.toInt() / 2
|
||||
val newCache = MemorySegmentCache(cacheSize)
|
||||
|
@ -113,14 +112,13 @@ class RandomAccessCallback private constructor(
|
|||
|
||||
override fun onGetSize(): Long {
|
||||
Logger.log.fine("onGetFileSize $url")
|
||||
if (cancellationSignal?.isCanceled == true)
|
||||
throw ErrnoException("onGetFileSize", OsConstants.EINTR)
|
||||
|
||||
throwIfCancelled("onGetFileSize")
|
||||
return fileSize
|
||||
}
|
||||
|
||||
override fun onRead(offset: Long, size: Int, data: ByteArray): Int {
|
||||
Logger.log.fine("onRead $url $offset $size")
|
||||
throwIfCancelled("onRead")
|
||||
|
||||
val progress =
|
||||
if (fileSize == 0L) // avoid division by zero
|
||||
|
@ -133,9 +131,6 @@ class RandomAccessCallback private constructor(
|
|||
notification.setProgress(100, progress, false).build()
|
||||
)
|
||||
|
||||
if (cancellationSignal?.isCanceled == true)
|
||||
throw ErrnoException("onRead", OsConstants.EINTR)
|
||||
|
||||
try {
|
||||
val docKey = DocumentKey(url, documentState)
|
||||
return cache.read(docKey, offset, size, data)
|
||||
|
@ -146,11 +141,13 @@ class RandomAccessCallback private constructor(
|
|||
}
|
||||
|
||||
override fun onWrite(offset: Long, size: Int, data: ByteArray): Int {
|
||||
Logger.log.fine("onWrite $url $offset $size")
|
||||
// ranged write requests not supported by WebDAV (yet)
|
||||
throw ErrnoException("onWrite", OsConstants.EROFS)
|
||||
}
|
||||
|
||||
override fun onRelease() {
|
||||
Logger.log.fine("onRelease")
|
||||
notificationManager.cancel(notificationTag, NotificationUtils.NOTIFY_WEBDAV_ACCESS)
|
||||
}
|
||||
|
||||
|
@ -175,8 +172,10 @@ class RandomAccessCallback private constructor(
|
|||
PAGE_SIZE,
|
||||
ifMatch
|
||||
) { response ->
|
||||
if (response.code != 206)
|
||||
throw DavException("Expected 206 Partial, got ${response.code} ${response.message}")
|
||||
if (response.code == 200) // server doesn't support ranged requests
|
||||
throw PartialContentNotSupportedException()
|
||||
else if (response.code != 206)
|
||||
throw HttpException(response)
|
||||
|
||||
result = response.body?.bytes()
|
||||
}
|
||||
|
@ -185,6 +184,13 @@ class RandomAccessCallback private constructor(
|
|||
}
|
||||
|
||||
|
||||
private fun throwIfCancelled(functionName: String) {
|
||||
if (cancellationSignal?.isCanceled == true) {
|
||||
Logger.log.warning("Random file access cancelled, throwing ErrnoException(EINTR)")
|
||||
throw ErrnoException(functionName, OsConstants.EINTR)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Exception.toErrNoException(functionName: String) =
|
||||
ErrnoException(functionName,
|
||||
when (this) {
|
||||
|
@ -195,6 +201,7 @@ class RandomAccessCallback private constructor(
|
|||
else -> OsConstants.EIO
|
||||
}
|
||||
is InterruptedIOException -> OsConstants.EINTR
|
||||
is PartialContentNotSupportedException -> OsConstants.EOPNOTSUPP
|
||||
else -> OsConstants.EIO
|
||||
}
|
||||
)
|
||||
|
@ -205,6 +212,8 @@ class RandomAccessCallback private constructor(
|
|||
val state: DocumentState
|
||||
)
|
||||
|
||||
class PartialContentNotSupportedException: Exception()
|
||||
|
||||
|
||||
/**
|
||||
* (2021/12/02) Currently Android's [StorageManager.openProxyFileDescriptor] has a memory leak:
|
||||
|
@ -239,16 +248,15 @@ class RandomAccessCallback private constructor(
|
|||
object GoStandby : Event
|
||||
object Close : Event
|
||||
}
|
||||
sealed class States : DefaultState() {
|
||||
object Active: States() {
|
||||
object Transferring: States()
|
||||
object Idle: States()
|
||||
}
|
||||
object Standby: States()
|
||||
object Closed: States(), FinalState
|
||||
}
|
||||
/* We don't use a sealed class for states here because the states would then be singletons, while we can have
|
||||
multiple instances of the state machine (which require multiple instances of the states, too). */
|
||||
val machine = createStdLibStateMachine {
|
||||
addInitialState(States.Active) {
|
||||
lateinit var activeIdleState: State
|
||||
lateinit var activeTransferringState: State
|
||||
lateinit var standbyState: State
|
||||
lateinit var closedState: State
|
||||
|
||||
initialState("active") {
|
||||
onEntry {
|
||||
_callback = RandomAccessCallback(context, httpClient, url, mimeType, headResponse, cancellationSignal)
|
||||
}
|
||||
|
@ -257,11 +265,11 @@ class RandomAccessCallback private constructor(
|
|||
_callback = null
|
||||
}
|
||||
|
||||
transition<Events.GoStandby>(targetState = States.Standby)
|
||||
transition<Events.Close>(targetState = States.Closed)
|
||||
transitionOn<Events.GoStandby> { targetState = { standbyState } }
|
||||
transitionOn<Events.Close> { targetState = { closedState } }
|
||||
|
||||
// active has two nested states: transferring (I/O running) and idle (starts timeout timer)
|
||||
addInitialState(States.Active.Idle) {
|
||||
activeIdleState = initialState("idle") {
|
||||
val timer: Timer = Timer(true)
|
||||
var timeout: TimerTask? = null
|
||||
|
||||
|
@ -278,21 +286,21 @@ class RandomAccessCallback private constructor(
|
|||
timer.cancel()
|
||||
}
|
||||
|
||||
transition<Events.Transfer>(targetState = States.Active.Transferring)
|
||||
transitionOn<Events.Transfer> { targetState = { activeTransferringState } }
|
||||
}
|
||||
|
||||
addState(States.Active.Transferring) {
|
||||
transition<Events.NowIdle>(targetState = States.Active.Idle)
|
||||
activeTransferringState = state("transferring") {
|
||||
transitionOn<Events.NowIdle> { targetState = { activeIdleState } }
|
||||
}
|
||||
}
|
||||
|
||||
addState(States.Standby) {
|
||||
transition<Events.Transfer>(targetState = States.Active.Transferring)
|
||||
transition<Events.NowIdle>(targetState = States.Active.Idle)
|
||||
transition<Events.Close>(targetState = States.Closed)
|
||||
standbyState = state("standby") {
|
||||
transitionOn<Events.Transfer> { targetState = { activeTransferringState } }
|
||||
transitionOn<Events.NowIdle> { targetState = { activeIdleState } }
|
||||
transitionOn<Events.Close> { targetState = { closedState } }
|
||||
}
|
||||
|
||||
addFinalState(States.Closed)
|
||||
closedState = finalState("closed")
|
||||
onFinished {
|
||||
shutdown()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue