move uri handling to dedicated class

This commit is contained in:
Jonathan Fisher 2020-11-13 14:01:53 +00:00
parent 199de8b3b0
commit 08842fd316
6 changed files with 190 additions and 58 deletions

View File

@ -0,0 +1,73 @@
package oppen.ariane.io.gemini
import android.net.Uri
import oppen.ariane.Ariane
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class AddressTests {
@Test
fun uriApiTests(){
var uri = Uri.parse(Ariane.DEFAULT_HOME_CAPSULE)
assertThat(uri.toString()).isEqualTo("gemini://gemini.circumlunar.space/~oppen/index.gmi")
assertThat(uri.isAbsolute).isTrue()
assertThat(uri.isHierarchical).isTrue()
assertThat(uri.isRelative).isFalse()
val lastSegment = uri.lastPathSegment
assertThat(lastSegment).isEqualTo("index.gmi")
uri = Uri.parse(uri.pathSegments.joinToString("/"))
assertThat(uri.toString()).isEqualTo("~oppen/index.gmi")
assertThat(uri.isRelative).isTrue()
}
@Test
fun arianeUriTests(){
val builder = AddressBuilder()
var uri = builder.request("gemini://gemini.circumlunar.space/~oppen/index.gmi").uri()
assertThat(uri.toString()).isEqualTo("gemini://gemini.circumlunar.space/~oppen/index.gmi")
assertThat(uri.path).isEqualTo("/~oppen/index.gmi")
uri = builder.request("/hello/index.gmi").uri()
assertThat(uri.toString()).isEqualTo("gemini://gemini.circumlunar.space/~oppen/hello/index.gmi")
uri = builder.request("/world/index.gmi").uri()
assertThat(uri.toString()).isEqualTo("gemini://gemini.circumlunar.space/~oppen/hello/world/index.gmi")
uri = builder.request("./foo/index.gmi").uri()
assertThat(uri.toString()).isEqualTo("gemini://gemini.circumlunar.space/~oppen/hello/world/foo/index.gmi")
uri = builder.request("bar/").uri()
assertThat(uri.toString()).isEqualTo("gemini://gemini.circumlunar.space/~oppen/hello/world/foo/bar/index.gmi")
}
//https://todo.sr.ht/~oppen/ariane/28
@Test
fun userTicketUriTests(){
val builder = AddressBuilder()
builder.request("gemini://mycapsule.com")
val relativeWithGmi = builder.request("art/index.gmi").uri()
assertThat(relativeWithGmi.toString()).isEqualTo("gemini://mycapsule.com/art/index.gmi")
builder.request("gemini://mycapsule.com")
val relativeWithoutGmi = builder.request("art/").uri()
assertThat(relativeWithoutGmi.toString()).isEqualTo("gemini://mycapsule.com/art/index.gmi")
}
@Test
fun doubleSlashTest(){
val builder = AddressBuilder()
val uri = builder.request("//mycapsule.com").uri()
assertThat(uri.toString()).isEqualTo("gemini://mycapsule.com")
}
}

View File

@ -2,6 +2,7 @@ package oppen
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.os.CountDownTimer
import android.view.View
import android.view.inputmethod.InputMethodManager
@ -28,6 +29,18 @@ fun String.toURI(): URI {
return URI.create(this)
}
fun URI.toUri(): Uri {
return Uri.parse(this.toString())
}
fun Uri.toURI(): URI {
return URI.create(this.toString())
}
fun Uri.isGemini(): Boolean{
return this.toString().startsWith("gemini://")
}
@SuppressLint("DefaultLocale")
fun String.endsWithImage(): Boolean{
return this.toLowerCase().endsWith(".png") ||
@ -36,6 +49,12 @@ fun String.endsWithImage(): Boolean{
this.toLowerCase().endsWith(".gif")
}
@SuppressLint("DefaultLocale")
fun String.isWeb(): Boolean{
return this.toLowerCase().startsWith("https://") ||
this.toLowerCase().startsWith("http://")
}
fun delay(ms: Long, action: () -> Unit){
object : CountDownTimer(ms, ms/2) {
override fun onTick(millisUntilFinished: Long) {}

View File

@ -0,0 +1,59 @@
package oppen.ariane.io.gemini
import android.net.Uri
import java.net.URI
class AddressBuilder{
private var uri: Uri = Uri.EMPTY
fun set(uri: Uri){
this.uri = uri
}
fun request(link: URI): AddressBuilder {
return request(link.toString())
}
fun request(link: String): AddressBuilder {
val linkUri = Uri.parse(link)
when {
linkUri.isAbsolute -> uri = linkUri
linkUri.toString().startsWith("//") -> uri = Uri.parse("gemini:$link")
else -> {
val currentAddress = uri.toString()
val directoryPath = when {
currentAddress.lastIndexOf("/") > 8 -> {
currentAddress.substring(0, currentAddress.lastIndexOf("/")+1)
}
else -> {
"${uri.scheme}://${uri.host}/"
}
}
var cleanedLink = when {
link.startsWith("./") -> {
link.substring(2)
}
link.startsWith("/") -> {
link.substring(1)
}
else -> {
link
}
}
if(link.endsWith("/")) cleanedLink += "index.gmi"
uri = Uri.parse("$directoryPath$cleanedLink")
}
}
return this
}
fun uri(): Uri{
return uri
}
}

View File

@ -6,11 +6,12 @@ import androidx.preference.PreferenceManager
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import oppen.ariane.io.GemState
import oppen.isGemini
import oppen.toURI
import oppen.toUri
import java.io.*
import java.net.ConnectException
import java.net.URI
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.*
const val GEMINI_SCHEME = "gemini"
@ -27,6 +28,8 @@ class GeminiDatasource(
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private var last: URI? = null
private val addressBuilder = AddressBuilder()
override fun request(uri: URI, onUpdate: (state: GemState) -> Unit) {
//Any inputted uri starting with a colon is an app-specific command, eg. :prefs :settings
@ -37,69 +40,42 @@ class GeminiDatasource(
when (uri.scheme) {
GEMINI_SCHEME -> {
addressBuilder.set(uri.toUri())
val cached = RuntimeCache.get(uri)
if (cached != null) {
last = uri
onUpdate(
GemState.ResponseGemtext(
uri,
cached.first,
cached.second
)
)
return
} else {
onUpdate(GemState.Requesting(uri))
when {
cached != null -> {
last = uri
onUpdate(GemState.ResponseGemtext(uri, cached.first, cached.second))
return
}
else -> {
onUpdate(GemState.Requesting(uri))
GlobalScope.launch {
geminiRequest(uri, onUpdate)
GlobalScope.launch {
geminiRequest(uri, onUpdate)
}
}
}
}
else -> {
val address = uri.toString()
val parsedUri = when {
address.startsWith("//") -> {
//just missing protocol
URI.create("gemini:$address")
}
address.startsWith("/") -> {
//internal navigation/relative link
val internalNav = "gemini://${last?.host}$address"
URI.create(internalNav)
}
address.startsWith("./") -> {
//internal navigation/relative link - with dot
val internalNav = "gemini://${last?.host}${address.substring(1)}"
URI.create(internalNav)
}
!address.contains("://") -> {
//looks like a relative link
val lastAddress = last.toString()
val relAddress = "${lastAddress.substring(
0,
lastAddress.lastIndexOf("/") + 1
)}$address"
URI.create(relAddress)
}
else -> {
onUpdate(GemState.NotGeminiRequest(uri))
return
}
}
val cached = RuntimeCache.get(parsedUri)
if(cached != null){
last = parsedUri
onUpdate(
GemState.ResponseGemtext(
parsedUri,
cached.first,
cached.second
val parsedUri = addressBuilder.request(uri).uri()
if(parsedUri.isGemini()){
val cached = RuntimeCache.get(parsedUri)
if(cached != null){
last = parsedUri.toURI()
onUpdate(
GemState.ResponseGemtext(
parsedUri.toURI(),
cached.first,
cached.second
)
)
)
}else{
request(parsedUri.toURI(), onUpdate)
}
}else{
request(parsedUri, onUpdate)
onUpdate(GemState.NotGeminiRequest(uri))
return
}
}
}

View File

@ -1,5 +1,6 @@
package oppen.ariane.io.gemini
import android.net.Uri
import androidx.collection.LruCache
import java.net.URI
@ -15,6 +16,7 @@ object RuntimeCache {
}
fun get(uri: URI): Pair<GeminiResponse.Header, List<String>>? = lruCache[uri.toString()]
fun get(uri: Uri): Pair<GeminiResponse.Header, List<String>>? = lruCache[uri.toString()]
fun clear() = lruCache.evictAll()

View File

@ -5,6 +5,7 @@ import android.view.View
import androidx.appcompat.widget.PopupMenu
import oppen.ariane.R
import oppen.endsWithImage
import oppen.isWeb
import java.net.URI
object LinkPopup {
@ -15,8 +16,10 @@ object LinkPopup {
val popup = PopupMenu(view.context, view)
val inflater: MenuInflater = popup.menuInflater
val uriStr = uri.toString()
when {
uri.toString().endsWithImage() -> inflater.inflate(R.menu.image_link_menu, popup.menu)
uriStr.endsWithImage() && !uriStr.isWeb() -> inflater.inflate(R.menu.image_link_menu, popup.menu)
else -> inflater.inflate(R.menu.link_menu, popup.menu)
}