From 3ff4758bbe8c3de169f93653badd6f8d2209c03c Mon Sep 17 00:00:00 2001 From: Jonathan Fisher Date: Fri, 13 Nov 2020 15:38:57 +0000 Subject: [PATCH] file download... --- app/src/main/AndroidManifest.xml | 11 ++ app/src/main/java/oppen/ariane/io/GemState.kt | 2 + .../java/oppen/ariane/io/gemini/Datasource.kt | 1 + .../ariane/io/gemini/GeminiDatasource.kt | 140 +++++------------- .../main/java/oppen/ariane/ui/GemActivity.kt | 87 +++++++---- .../main/java/oppen/ariane/ui/GemViewModel.kt | 7 + .../ariane/ui/settings/SettingsFragment.kt | 3 - app/src/main/res/values-night/styles.xml | 2 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 7 + app/src/main/res/xml/provider_paths.xml | 4 + 11 files changed, 134 insertions(+), 131 deletions(-) create mode 100644 app/src/main/res/xml/provider_paths.xml 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 @@ + + + +