From 08842fd316c60a435935a613b1383b672c10ef19 Mon Sep 17 00:00:00 2001 From: Jonathan Fisher Date: Fri, 13 Nov 2020 14:01:53 +0000 Subject: [PATCH] move uri handling to dedicated class --- .../oppen/ariane/io/gemini/AddressTests.kt | 73 +++++++++++++++ app/src/main/java/oppen/Extensions.kt | 19 ++++ .../oppen/ariane/io/gemini/AddressBuilder.kt | 59 ++++++++++++ .../ariane/io/gemini/GeminiDatasource.kt | 90 +++++++------------ .../oppen/ariane/io/gemini/RuntimeCache.kt | 2 + .../oppen/ariane/ui/modals_menus/LinkPopup.kt | 5 +- 6 files changed, 190 insertions(+), 58 deletions(-) create mode 100644 app/src/androidTest/java/oppen/ariane/io/gemini/AddressTests.kt create mode 100644 app/src/main/java/oppen/ariane/io/gemini/AddressBuilder.kt diff --git a/app/src/androidTest/java/oppen/ariane/io/gemini/AddressTests.kt b/app/src/androidTest/java/oppen/ariane/io/gemini/AddressTests.kt new file mode 100644 index 0000000..d22cd03 --- /dev/null +++ b/app/src/androidTest/java/oppen/ariane/io/gemini/AddressTests.kt @@ -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") + } +} \ No newline at end of file diff --git a/app/src/main/java/oppen/Extensions.kt b/app/src/main/java/oppen/Extensions.kt index e9c68b6..e945e92 100644 --- a/app/src/main/java/oppen/Extensions.kt +++ b/app/src/main/java/oppen/Extensions.kt @@ -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) {} diff --git a/app/src/main/java/oppen/ariane/io/gemini/AddressBuilder.kt b/app/src/main/java/oppen/ariane/io/gemini/AddressBuilder.kt new file mode 100644 index 0000000..16f8245 --- /dev/null +++ b/app/src/main/java/oppen/ariane/io/gemini/AddressBuilder.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/oppen/ariane/io/gemini/GeminiDatasource.kt b/app/src/main/java/oppen/ariane/io/gemini/GeminiDatasource.kt index 2fdd3df..fa3581b 100644 --- a/app/src/main/java/oppen/ariane/io/gemini/GeminiDatasource.kt +++ b/app/src/main/java/oppen/ariane/io/gemini/GeminiDatasource.kt @@ -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 } } } diff --git a/app/src/main/java/oppen/ariane/io/gemini/RuntimeCache.kt b/app/src/main/java/oppen/ariane/io/gemini/RuntimeCache.kt index 950910f..abe012d 100644 --- a/app/src/main/java/oppen/ariane/io/gemini/RuntimeCache.kt +++ b/app/src/main/java/oppen/ariane/io/gemini/RuntimeCache.kt @@ -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>? = lruCache[uri.toString()] + fun get(uri: Uri): Pair>? = lruCache[uri.toString()] fun clear() = lruCache.evictAll() diff --git a/app/src/main/java/oppen/ariane/ui/modals_menus/LinkPopup.kt b/app/src/main/java/oppen/ariane/ui/modals_menus/LinkPopup.kt index 6f6ae1a..5d8f985 100644 --- a/app/src/main/java/oppen/ariane/ui/modals_menus/LinkPopup.kt +++ b/app/src/main/java/oppen/ariane/ui/modals_menus/LinkPopup.kt @@ -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) }