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:
Ricki Hirner 2023-11-05 20:36:53 +01:00
parent fbe0c4451b
commit cf9340107f
No known key found for this signature in database
GPG key ID: 79A019FCAAEDD3AA
2 changed files with 48 additions and 37 deletions

View file

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

View file

@ -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()
}