diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 92f7412..f215b7f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -39,6 +39,17 @@
android:label="@string/settings"
android:theme="@style/SettingsTheme"/>
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/oppen/ariane/io/GemState.kt b/app/src/main/java/oppen/ariane/io/GemState.kt
index f65b45e..d46f2f4 100644
--- a/app/src/main/java/oppen/ariane/io/GemState.kt
+++ b/app/src/main/java/oppen/ariane/io/GemState.kt
@@ -14,6 +14,8 @@ sealed class GemState {
data class ResponseText(val uri: URI, val header: GeminiResponse.Header, val content: String) : GemState()
data class ResponseImage(val uri: URI, val header: GeminiResponse.Header, val cacheUri: Uri) : GemState()
data class ResponseAudio(val uri: URI, val header: GeminiResponse.Header, val cacheUri: Uri) : GemState()
+ data class ResponseBinary(val uri: URI, val header: GeminiResponse.Header, val cacheUri: Uri) : GemState()
+ data class ResponseUnknownMime(val uri: URI, val header: GeminiResponse.Header) : GemState()
data class ResponseError(val header: GeminiResponse.Header): GemState()
object Blank: GemState()
diff --git a/app/src/main/java/oppen/ariane/io/gemini/Datasource.kt b/app/src/main/java/oppen/ariane/io/gemini/Datasource.kt
index 12a39cd..0b5dfb2 100644
--- a/app/src/main/java/oppen/ariane/io/gemini/Datasource.kt
+++ b/app/src/main/java/oppen/ariane/io/gemini/Datasource.kt
@@ -5,6 +5,7 @@ import oppen.ariane.io.GemState
import java.net.URI
interface Datasource {
+ fun request(uri: URI, forceDownload: Boolean, onUpdate: (state: GemState) -> Unit)
fun request(uri: URI, onUpdate: (state: GemState) -> Unit)
companion object{
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 fa3581b..fa84efd 100644
--- a/app/src/main/java/oppen/ariane/io/gemini/GeminiDatasource.kt
+++ b/app/src/main/java/oppen/ariane/io/gemini/GeminiDatasource.kt
@@ -16,12 +16,6 @@ import javax.net.ssl.*
const val GEMINI_SCHEME = "gemini"
-/**
- *
- *
- * @param protocol see: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SSLContext
- *
- */
class GeminiDatasource(
private val context: Context): Datasource {
@@ -30,8 +24,12 @@ class GeminiDatasource(
private val addressBuilder = AddressBuilder()
- override fun request(uri: URI, onUpdate: (state: GemState) -> Unit) {
+ private var forceDownload = false
+ override fun request(uri: URI, onUpdate: (state: GemState) -> Unit) = request(uri, false, onUpdate)
+
+ override fun request(uri: URI, forceDownload: Boolean, onUpdate: (state: GemState) -> Unit) {
+ this.forceDownload = forceDownload
//Any inputted uri starting with a colon is an app-specific command, eg. :prefs :settings
if(uri.toString().startsWith(":")){
onUpdate(GemState.AppQuery(uri))
@@ -63,15 +61,9 @@ class GeminiDatasource(
val cached = RuntimeCache.get(parsedUri)
if(cached != null){
last = parsedUri.toURI()
- onUpdate(
- GemState.ResponseGemtext(
- parsedUri.toURI(),
- cached.first,
- cached.second
- )
- )
+ onUpdate(GemState.ResponseGemtext(parsedUri.toURI(), cached.first, cached.second))
}else{
- request(parsedUri.toURI(), onUpdate)
+ request(parsedUri.toURI(), forceDownload, onUpdate)
}
}else{
onUpdate(GemState.NotGeminiRequest(uri))
@@ -97,10 +89,9 @@ class GeminiDatasource(
println("REQ_PROTOCOL: $protocol")
//todo - extract and reuse this
- val sslContext = if(protocol == "TLS_ALL"){
- SSLContext.getInstance("TLS")
- }else{
- SSLContext.getInstance(protocol)
+ val sslContext = when (protocol) {
+ "TLS_ALL" -> SSLContext.getInstance("TLS")
+ else -> SSLContext.getInstance(protocol)
}
sslContext.init(null, DummyTrustManager.get(), null)
@@ -119,25 +110,11 @@ class GeminiDatasource(
socket.startHandshake()
}catch (ce: ConnectException){
println("socket error: $ce")
- onUpdate(
- GemState.ResponseError(
- GeminiResponse.Header(
- -1,
- ce.message ?: ce.toString()
- )
- )
- )
+ onUpdate(GemState.ResponseError(GeminiResponse.Header(-1, ce.message ?: ce.toString())))
return
}catch (she: SSLHandshakeException){
println("socket error: $she")
- onUpdate(
- GemState.ResponseError(
- GeminiResponse.Header(
- -2,
- she.message ?: she.toString()
- )
- )
- )
+ onUpdate(GemState.ResponseError(GeminiResponse.Header(-2, she.message ?: she.toString())))
return
}
@@ -151,14 +128,7 @@ class GeminiDatasource(
outWriter.flush()
if (outWriter.checkError()) {
- onUpdate(
- GemState.ResponseError(
- GeminiResponse.Header(
- -1,
- "Print Writer Error"
- )
- )
- )
+ onUpdate(GemState.ResponseError(GeminiResponse.Header(-1, "Print Writer Error")))
outWriter.close()
return
}
@@ -176,18 +146,21 @@ class GeminiDatasource(
when {
header.code == GeminiResponse.INPUT -> onUpdate(GemState.ResponseInput(uri, header))
- header.code == GeminiResponse.REDIRECT -> request(URI.create(header.meta), onUpdate)
+ header.code == GeminiResponse.REDIRECT -> request(URI.create(header.meta), forceDownload, onUpdate)
header.code != GeminiResponse.SUCCESS -> onUpdate(GemState.ResponseError(header))
- header.meta.startsWith("text/gemini") -> getGemtext(
- bufferedReader,
- uri,
- header,
- onUpdate
- )
+ header.meta.startsWith("text/gemini") -> getGemtext(bufferedReader, uri, header, onUpdate)
header.meta.startsWith("text/") -> getString(socket, uri, header, onUpdate)
header.meta.startsWith("image/") -> getBinary(socket, uri, header, onUpdate)
header.meta.startsWith("audio/") -> getBinary(socket, uri, header, onUpdate)
- else -> onUpdate(GemState.ResponseError(header))
+ else -> {
+ //File served over Gemini but not handled in-app, eg .pdf
+ if(forceDownload){
+ getBinary(socket, uri, header, onUpdate)
+ }else{
+ onUpdate(GemState.ResponseUnknownMime(uri, header))
+ }
+
+ }
}
//Close input
@@ -202,12 +175,7 @@ class GeminiDatasource(
socket.close()
}
- private fun getGemtext(
- reader: BufferedReader,
- uri: URI,
- header: GeminiResponse.Header,
- onUpdate: (state: GemState) -> Unit
- ){
+ private fun getGemtext(reader: BufferedReader, uri: URI, header: GeminiResponse.Header, onUpdate: (state: GemState) -> Unit){
val lines = mutableListOf()
@@ -222,28 +190,21 @@ class GeminiDatasource(
onUpdate(GemState.ResponseGemtext(uri, header, processed))
}
- private fun getString(
- socket: SSLSocket?,
- uri: URI,
- header: GeminiResponse.Header,
- onUpdate: (state: GemState) -> Unit
- ){
- val content = socket?.inputStream?.bufferedReader().use { reader -> reader?.readText() }
+ private fun getString(socket: SSLSocket?, uri: URI, header: GeminiResponse.Header, onUpdate: (state: GemState) -> Unit){
+ val content = socket?.inputStream?.bufferedReader().use {
+ reader -> reader?.readText()
+ }
socket?.close()
onUpdate(GemState.ResponseText(uri, header, content ?: "Error fetching content"))
}
- private fun getBinary(
- socket: SSLSocket?,
- uri: URI,
- header: GeminiResponse.Header,
- onUpdate: (state: GemState) -> Unit
- ){
+ private fun getBinary(socket: SSLSocket?, uri: URI, header: GeminiResponse.Header, onUpdate: (state: GemState) -> Unit){
var filename: String? = null
val fileSegmentIndex: Int = uri.path.lastIndexOf('/')
- if (fileSegmentIndex != -1) {
- filename = uri.path.substring(fileSegmentIndex + 1)
+
+ when {
+ fileSegmentIndex != -1 -> filename = uri.path.substring(fileSegmentIndex + 1)
}
val host = uri.host.replace(".", "_")
@@ -255,22 +216,10 @@ class GeminiDatasource(
when {
cacheFile.exists() -> {
when {
- header.meta.startsWith("image/") -> onUpdate(
- GemState.ResponseImage(
- uri,
- header,
- cacheFile.toUri()
- )
- )
- header.meta.startsWith("audio/") -> onUpdate(
- GemState.ResponseAudio(
- uri,
- header,
- cacheFile.toUri()
- )
- )
+ header.meta.startsWith("image/") -> onUpdate(GemState.ResponseImage(uri, header, cacheFile.toUri()))
+ header.meta.startsWith("audio/") -> onUpdate(GemState.ResponseAudio(uri, header, cacheFile.toUri()))
+ else -> onUpdate(GemState.ResponseBinary(uri, header, cacheFile.toUri()))
}
-
}
else -> {
cacheFile.createNewFile()
@@ -280,20 +229,9 @@ class GeminiDatasource(
}
when {
- header.meta.startsWith("image/") -> onUpdate(
- GemState.ResponseImage(
- uri,
- header,
- cacheFile.toUri()
- )
- )
- header.meta.startsWith("audio/") -> onUpdate(
- GemState.ResponseAudio(
- uri,
- header,
- cacheFile.toUri()
- )
- )
+ header.meta.startsWith("image/") -> onUpdate(GemState.ResponseImage(uri, header, cacheFile.toUri()))
+ header.meta.startsWith("audio/") -> onUpdate(GemState.ResponseAudio(uri, header, cacheFile.toUri()))
+ else -> onUpdate(GemState.ResponseBinary(uri, header, cacheFile.toUri()))
}
}
}
diff --git a/app/src/main/java/oppen/ariane/ui/GemActivity.kt b/app/src/main/java/oppen/ariane/ui/GemActivity.kt
index 998caa6..ac38e11 100644
--- a/app/src/main/java/oppen/ariane/ui/GemActivity.kt
+++ b/app/src/main/java/oppen/ariane/ui/GemActivity.kt
@@ -1,6 +1,8 @@
package oppen.ariane.ui
+import android.app.DownloadManager
import android.content.ActivityNotFoundException
+import android.content.DialogInterface
import android.content.Intent
import android.media.MediaPlayer
import android.net.Uri
@@ -34,6 +36,7 @@ import oppen.ariane.ui.modals_menus.input.InputDialog
import oppen.ariane.ui.modals_menus.overflow.OverflowPopup
import oppen.ariane.ui.settings.SettingsActivity
import oppen.hideKeyboard
+import oppen.toURI
import oppen.visibleRetainingSpace
import java.io.File
import java.io.FileInputStream
@@ -42,6 +45,7 @@ import java.net.URLEncoder
const val CREATE_IMAGE_FILE_REQ = 628
const val CREATE_AUDIO_FILE_REQ = 629
+const val CREATE_BINARY_FILE_REQ = 630
class GemActivity : AppCompatActivity() {
@@ -126,10 +130,25 @@ class GemActivity : AppCompatActivity() {
is GemState.ResponseText -> renderText(state)
is GemState.ResponseImage -> renderImage(state)
is GemState.ResponseAudio -> renderAudio(state)
+ is GemState.ResponseBinary -> renderBinary(state)
is GemState.Blank -> {
binding.addressEdit.setText("")
adapter.render(arrayListOf())
}
+ is GemState.ResponseUnknownMime -> {
+ runOnUiThread {
+ loadingView(false)
+ AlertDialog.Builder(this, R.style.AppDialogTheme)
+ .setTitle(R.string.unknown_mime_dialog_title)
+ .setMessage("Address: ${state.uri}\nMeta: ${state.header.meta}")
+ .setPositiveButton("Download") { _, _ ->
+ loadingView(true)
+ model.requestBinaryDownload(state.uri)
+ }
+ .setNegativeButton("Cancel") { _, _ -> }
+ .show()
+ }
+ }
}
}
@@ -138,17 +157,9 @@ class GemActivity : AppCompatActivity() {
EditorInfo.IME_ACTION_GO -> {
val input = binding.addressEdit.text.toString()
- if (input.startsWith("gemini://")) {
- model.request(input)
- } else {
- model.request(
- "${Ariane.GEMINI_USER_SEARCH_BASE}${
- URLEncoder.encode(
- input,
- "UTF-8"
- )
- }"
- )
+ when {
+ input.startsWith("gemini://") -> model.request(input)
+ else -> model.request("${Ariane.GEMINI_USER_SEARCH_BASE}${URLEncoder.encode(input, "UTF-8")}")
}
binding.addressEdit.hideKeyboard()
@@ -310,6 +321,7 @@ class GemActivity : AppCompatActivity() {
var imageState: GemState.ResponseImage? = null
var audioState: GemState.ResponseAudio? = null
+ var binaryState: GemState.ResponseBinary? = null
private fun renderAudio(state: GemState.ResponseAudio) = runOnUiThread {
loadingView(false)
@@ -335,36 +347,57 @@ class GemActivity : AppCompatActivity() {
}
}
+ private fun renderBinary(state: GemState.ResponseBinary) = runOnUiThread{
+ loadingView(false)
+ binaryState = state
+ val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+ intent.type = state.header.meta
+ intent.putExtra(Intent.EXTRA_TITLE, File(state.uri.path).name)
+ startActivityForResult(intent, CREATE_BINARY_FILE_REQ)
+ }
+
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
- if(resultCode == RESULT_OK && (requestCode == CREATE_IMAGE_FILE_REQ || requestCode == CREATE_AUDIO_FILE_REQ)){
- if(imageState == null && audioState == null) return
-
+ if(resultCode == RESULT_OK && (requestCode == CREATE_IMAGE_FILE_REQ || requestCode == CREATE_AUDIO_FILE_REQ || requestCode == CREATE_BINARY_FILE_REQ)){
//todo - tidy this mess up...
+ if(imageState == null && audioState == null && binaryState == null) return
data?.data?.let{ uri ->
val cachedFile = when {
- imageState != null -> {
- File(imageState!!.cacheUri.path ?: "")
- }
+ imageState != null -> File(imageState!!.cacheUri.path ?: "")
+ audioState != null -> File(audioState!!.cacheUri.path ?: "")
+ binaryState != null -> File(binaryState!!.cacheUri.path ?: "")
else -> {
- File(audioState!!.cacheUri.path ?: "")
+ println("File download error - no state object exists")
+ showAlert("File download error - no state object exists")
+ null
}
}
- contentResolver.openFileDescriptor(uri, "w")?.use { fileDescriptor ->
- FileOutputStream(fileDescriptor.fileDescriptor).use { destOutput ->
- val sourceChannel = FileInputStream(cachedFile).channel
- val destChannel = destOutput.channel
- sourceChannel.transferTo(0, sourceChannel.size(), destChannel)
- sourceChannel.close()
- destChannel.close()
+
+ cachedFile?.let{
+ contentResolver.openFileDescriptor(uri, "w")?.use { fileDescriptor ->
+ FileOutputStream(fileDescriptor.fileDescriptor).use { destOutput ->
+ val sourceChannel = FileInputStream(cachedFile).channel
+ val destChannel = destOutput.channel
+ sourceChannel.transferTo(0, sourceChannel.size(), destChannel)
+ sourceChannel.close()
+ destChannel.close()
+
+ cachedFile.deleteOnExit()
+
+ if(binaryState != null){
+ startActivity(Intent(DownloadManager.ACTION_VIEW_DOWNLOADS))
+ }else{
+ Snackbar.make(binding.root, "File saved to device", Snackbar.LENGTH_SHORT).show()
+ }
+ }
}
}
}
- Snackbar.make(binding.root, "File saved to device", Snackbar.LENGTH_SHORT).show()
-
imageState = null
audioState = null
+ binaryState = null
}
}
diff --git a/app/src/main/java/oppen/ariane/ui/GemViewModel.kt b/app/src/main/java/oppen/ariane/ui/GemViewModel.kt
index 4f39477..ceaec13 100644
--- a/app/src/main/java/oppen/ariane/ui/GemViewModel.kt
+++ b/app/src/main/java/oppen/ariane/ui/GemViewModel.kt
@@ -43,6 +43,13 @@ class GemViewModel: ViewModel() {
}
}
+ fun requestBinaryDownload(uri: URI) {
+ gemini.request(uri, true){ state ->
+ onState(state)
+ }
+ }
+
+ //todo - same action as above... refactor
fun requestInlineImage(uri: URI, onImageReady: (cacheUri: Uri?) -> Unit){
gemini.request(uri){ state ->
when (state) {
diff --git a/app/src/main/java/oppen/ariane/ui/settings/SettingsFragment.kt b/app/src/main/java/oppen/ariane/ui/settings/SettingsFragment.kt
index 18af243..db889ab 100644
--- a/app/src/main/java/oppen/ariane/ui/settings/SettingsFragment.kt
+++ b/app/src/main/java/oppen/ariane/ui/settings/SettingsFragment.kt
@@ -1,12 +1,10 @@
package oppen.ariane.ui.settings
import android.os.Bundle
-import android.text.InputType
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.view.inputmethod.EditorInfo
-import android.widget.EditText
import androidx.core.content.ContextCompat
import androidx.preference.*
import oppen.ariane.Ariane
@@ -16,7 +14,6 @@ import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
-
class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener {
lateinit var protocols: Array
diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml
index 8f1e6db..8e99719 100644
--- a/app/src/main/res/values-night/styles.xml
+++ b/app/src/main/res/values-night/styles.xml
@@ -17,4 +17,6 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8e23692..3f27aa6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -36,4 +36,5 @@
Delete
Move down
Move up
+ Unknown Mime Type
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index b4bf444..d935efc 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -21,6 +21,13 @@
- true
+
+
diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml
new file mode 100644
index 0000000..8d13fa1
--- /dev/null
+++ b/app/src/main/res/xml/provider_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+