@ -1,5 +1,6 @@
package dev.lowrespalmtree.comet
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.util.Log
@ -8,16 +9,19 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager
import dev.lowrespalmtree.comet.utils.joinUrls
import dev.lowrespalmtree.comet.utils.downloadMedia
import dev.lowrespalmtree.comet.utils.resolveLinkUri
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.onSuccess
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import java.nio.charset.Charset
class PageViewModel ( @Suppress ( " unused " ) private val savedStateHandle : SavedStateHandle ) :
ViewModel ( ) {
class PageViewModel (
@Suppress ( " unused " ) private val savedStateHandle : SavedStateHandle
) : ViewModel ( ) {
/** Currently viewed page URL. */
var currentUrl : String = " "
@ -42,14 +46,37 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
/** Lines for the current page. */
private var linesList = ArrayList < Line > ( )
/** Page state to be reflected on the UI (e.g. loading bar). */
enum class State {
IDLE , CONNECTING , RECEIVING
}
/** Generic event class to notify observers with. The handled flag avoids repeated usage. */
abstract class Event ( var handled : Boolean = false )
/** An user input has been requested from the URI, with this prompt. */
data class InputEvent ( val uri : Uri , val prompt : String ) : Event ( )
/** The server responded with a success code and *has finished* its response. */
data class SuccessEvent ( val uri : String ) : Event ( )
/** The server responded with a success code and a binary MIME type (not delivered yet). */
data class BinaryEvent (
val uri : Uri ,
val response : Response ,
val mimeType : MimeType
) : Event ( )
/** A file has been completely downloaded. */
data class DownloadCompletedEvent (
val uri : Uri ,
val mimeType : MimeType
) : Event ( )
/** The server is redirecting us. */
data class RedirectEvent ( val uri : String , val redirects : Int ) : Event ( )
/** The server responded with a failure code or we encountered a local issue. */
data class FailureEvent (
val short : String ,
val details : String ,
@ -59,7 +86,9 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
/ * *
* Perform a request against this URI .
*
* The URI must be valid , absolute and with a gemini scheme .
* @param uri URI to open ; must be valid , absolute and with a gemini scheme
* @param context Context used to retrieve user preferences , not stored
* @param redirects current number of redirections operated
* /
@ExperimentalCoroutinesApi
fun sendGeminiRequest (
@ -136,16 +165,36 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
}
}
/** Notify observers that an error happened, with a generic short message. Set state to idle. */
private fun signalError ( message : String ) {
event . postValue ( FailureEvent ( " Error " , message ) )
state . postValue ( State . IDLE )
}
/** Notify observers that user input has been requested. */
private fun handleInputResponse ( response : Response , uri : Uri ) {
event . postValue ( InputEvent ( uri , response . meta ) )
state . postValue ( State . IDLE )
}
/** Continue processing a successful response by looking at the provided MIME type. */
@ExperimentalCoroutinesApi
private suspend fun handleSuccessResponse ( response : Response , uri : Uri ) {
val mimeType = MimeType . from ( response . meta ) ?: MimeType . DEFAULT // Spec. section 3.3 last §
when ( mimeType . main ) {
" text " -> {
if ( mimeType . sub == " gemini " )
handleSuccessGemtextResponse ( response , uri )
else
handleSuccessGenericTextResponse ( response , uri )
}
else -> event . postValue ( BinaryEvent ( uri , response , mimeType ) )
}
}
/** Receive Gemtext data, parse it and send the lines to observers. */
@ExperimentalCoroutinesApi
private suspend fun handleSuccessGemtextResponse ( response : Response , uri : Uri ) {
state . postValue ( State . RECEIVING )
val uriString = uri . toString ( )
@ -163,9 +212,7 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
lineChannelResult . onSuccess { line ->
if ( line is LinkLine ) {
// Mark visited links here as we have a access to the history.
val fullUrl =
if ( Uri . parse ( line . url ) . isAbsolute ) line . url
else joinUrls ( uriString , line . url ) . toString ( )
val fullUrl = resolveLinkUri ( line . url , uriString ) . toString ( )
if ( History . contains ( fullUrl ) )
line . visited = true
}
@ -195,15 +242,26 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
// We record the history entry here: it's nice because we have the main title available
// and we're already in a coroutine for database access.
History . record ( uri . to String( ) , mainTitle )
event . postValue ( SuccessEvent ( uri . to String( ) ) )
History . record ( uri String, mainTitle )
event . postValue ( SuccessEvent ( uri String) )
state . postValue ( State . IDLE )
}
/** Receive generic text data (e.g. text/plain) and send it to observers. */
@ExperimentalCoroutinesApi
private suspend fun handleSuccessGenericTextResponse ( response : Response , uri : Uri ) =
handleSuccessGemtextResponse ( response , uri ) // TODO render plain text as... something else?
/** Notify observers that a redirect has been returned. */
private fun handleRedirectResponse ( response : Response , redirects : Int ) {
event . postValue ( RedirectEvent ( response . meta , redirects ) )
}
/ * *
* Provide an error message to the user corresponding to the error code .
*
* TODO This requires a lot of localisation .
* /
private fun handleErrorResponse ( response : Response ) {
val briefMessage = when ( response . code ) {
Response . Code . TEMPORARY _FAILURE -> " 40 Temporary failure "
@ -235,6 +293,33 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
if ( response . code != Response . Code . SLOW _DOWN && response . meta . isNotEmpty ( ) )
serverMessage = response . meta
event . postValue ( FailureEvent ( briefMessage , longMessage , serverMessage ) )
state . postValue ( State . IDLE )
}
/** Download response content as a file. */
@ExperimentalCoroutinesApi
fun downloadResponse (
channel : Channel < ByteArray > ,
uri : Uri ,
mimeType : MimeType ,
contentResolver : ContentResolver
) {
when ( mimeType . main ) {
" image " , " audio " , " video " -> {
downloadMedia (
channel , uri , mimeType , viewModelScope , contentResolver ,
onSuccess = { mediaUri ->
event . postValue ( DownloadCompletedEvent ( mediaUri , mimeType ) )
state . postValue ( State . IDLE )
} ,
onError = { msg -> signalError ( " Download failed: $msg " ) }
)
}
else -> {
// TODO use SAF
signalError ( " MIME type unsupported yet: ${mimeType.main} ( \" ${mimeType.short} \" ) " )
}
}
}
companion object {