ariane/app/src/main/java/oppen/ariane/ui/GemActivity.kt

413 lines
16 KiB
Kotlin
Raw Normal View History

package oppen.ariane.ui
2020-08-15 14:52:27 +00:00
2020-11-13 15:38:57 +00:00
import android.app.DownloadManager
2020-11-08 22:42:24 +00:00
import android.content.ActivityNotFoundException
import android.content.Intent
2020-08-25 11:57:35 +00:00
import android.media.MediaPlayer
import android.net.Uri
2020-08-15 14:52:27 +00:00
import android.os.Bundle
import android.view.inputmethod.EditorInfo
import androidx.activity.viewModels
2020-08-18 19:02:16 +00:00
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
2020-08-15 14:52:27 +00:00
import androidx.databinding.DataBindingUtil
import androidx.preference.PreferenceManager
2020-08-15 14:52:27 +00:00
import androidx.recyclerview.widget.LinearLayoutManager
2020-08-18 10:40:10 +00:00
import com.google.android.material.snackbar.Snackbar
import oppen.ariane.Ariane
2020-11-07 23:03:53 +00:00
import oppen.ariane.R
import oppen.ariane.databinding.ActivityGemBinding
import oppen.ariane.io.GemState
import oppen.ariane.io.database.ArianeDatabase
import oppen.ariane.io.database.bookmarks.BookmarksDatasource
import oppen.ariane.io.gemini.Datasource
import oppen.ariane.io.gemini.GeminiResponse
import oppen.ariane.io.gemini.RuntimeCache
import oppen.ariane.ui.audio_player.AudioPlayer
import oppen.ariane.ui.bookmarks.BookmarkDialog
import oppen.ariane.ui.bookmarks.BookmarksDialog
import oppen.ariane.ui.content_image.ImageDialog
import oppen.ariane.ui.content_text.TextDialog
2020-11-07 23:03:53 +00:00
import oppen.ariane.ui.modals_menus.LinkPopup
import oppen.ariane.ui.modals_menus.about.AboutDialog
import oppen.ariane.ui.modals_menus.history.HistoryDialog
import oppen.ariane.ui.modals_menus.input.InputDialog
import oppen.ariane.ui.modals_menus.overflow.OverflowPopup
2020-11-09 22:02:23 +00:00
import oppen.ariane.ui.settings.SettingsActivity
2020-08-20 17:24:23 +00:00
import oppen.hideKeyboard
2020-08-16 21:30:24 +00:00
import oppen.visibleRetainingSpace
2020-08-21 15:46:08 +00:00
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
2020-08-20 17:24:23 +00:00
import java.net.URLEncoder
2020-08-15 14:52:27 +00:00
2020-08-21 15:46:08 +00:00
const val CREATE_IMAGE_FILE_REQ = 628
2020-08-21 15:59:07 +00:00
const val CREATE_AUDIO_FILE_REQ = 629
2020-11-13 15:38:57 +00:00
const val CREATE_BINARY_FILE_REQ = 630
2020-09-10 16:33:10 +00:00
class GemActivity : AppCompatActivity() {
2020-08-15 14:52:27 +00:00
2020-08-20 14:57:38 +00:00
private var inSearch = false
2020-08-25 11:57:35 +00:00
private val mediaPlayer = MediaPlayer()
private lateinit var bookmarkDatasource: BookmarksDatasource
2020-09-10 16:33:10 +00:00
private val model by viewModels<GemViewModel>()
private lateinit var binding: ActivityGemBinding
2020-11-09 19:30:53 +00:00
private val adapter = GemtextAdapter { adapter, uri, longTap, position: Int, view ->
if(longTap){
2020-11-09 19:30:53 +00:00
LinkPopup.show(view, uri){ menuId ->
2020-08-17 20:25:39 +00:00
when (menuId) {
2020-11-09 19:30:53 +00:00
R.id.link_menu_load_image -> {
loadingView(true)
model.requestInlineImage(uri){ imageUri ->
imageUri?.let{
runOnUiThread {
loadingView(false)
adapter.loadImage(position, imageUri)
}
}
}
2020-11-09 19:30:53 +00:00
}
R.id.link_menu_copy -> {
Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, uri.toString())
type = "text/plain"
startActivity(Intent.createChooser(this, null))
}
2020-08-17 20:25:39 +00:00
}
}
}
}else{
2020-08-20 14:57:38 +00:00
//Reset input text hint after user has been searching
if(inSearch) {
binding.addressEdit.hint = getString(R.string.main_input_hint)
inSearch = false
}
model.request(uri)
}
2020-08-15 14:52:27 +00:00
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val db = ArianeDatabase(applicationContext)
bookmarkDatasource = db.bookmarks()
2020-09-10 16:33:10 +00:00
binding = DataBindingUtil.setContentView(this, R.layout.activity_gem)
2020-08-15 14:52:27 +00:00
binding.viewmodel = model
binding.lifecycleOwner = this
binding.gemtextRecycler.layoutManager = LinearLayoutManager(this)
binding.gemtextRecycler.adapter = adapter
2020-11-07 23:03:53 +00:00
model.initialise(
home = PreferenceManager.getDefaultSharedPreferences(this).getString("home_capsule", Ariane.DEFAULT_HOME_CAPSULE) ?: Ariane.DEFAULT_HOME_CAPSULE,
gemini = Datasource.factory(this, db.history()),
db = db,
onState = this::handleState)
2020-08-15 14:52:27 +00:00
binding.addressEdit.setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_GO -> {
2020-08-20 14:57:38 +00:00
val input = binding.addressEdit.text.toString()
2020-11-13 15:38:57 +00:00
when {
input.startsWith("gemini://") -> model.request(input)
else -> model.request("${Ariane.GEMINI_USER_SEARCH_BASE}${URLEncoder.encode(input, "UTF-8")}")
2020-08-20 14:57:38 +00:00
}
2020-08-20 17:24:23 +00:00
binding.addressEdit.hideKeyboard()
2020-08-15 14:52:27 +00:00
return@setOnEditorActionListener true
}
else -> return@setOnEditorActionListener false
}
}
2020-08-15 21:12:17 +00:00
2020-08-18 16:17:04 +00:00
binding.more.setOnClickListener {
2020-11-07 23:03:53 +00:00
OverflowPopup.show(binding.more){ menuId ->
2020-08-18 16:17:04 +00:00
when (menuId) {
2020-08-20 14:57:38 +00:00
R.id.overflow_menu_search -> {
binding.addressEdit.hint = getString(R.string.main_input_search_hint)
binding.addressEdit.text?.clear()
binding.addressEdit.requestFocus()
inSearch = true
}
2020-09-07 21:25:36 +00:00
R.id.overflow_menu_bookmark -> {
2020-09-11 19:49:30 +00:00
println("Bookmark: ---------------------------")
val name = adapter.inferTitle()
2020-11-07 23:03:53 +00:00
BookmarkDialog(
this,
BookmarkDialog.mode_new,
bookmarkDatasource,
binding.addressEdit.text.toString(),
name ?: ""
) { _, _ ->
bookmarkDatasource.get { bookmarks ->
2020-11-07 23:03:53 +00:00
bookmarks.forEach { bookmark ->
println("Bookmark: ${bookmark.label}: ${bookmark.uri}")
}
}
}.show()
2020-09-07 21:25:36 +00:00
}
R.id.overflow_menu_bookmarks -> {
2020-09-11 19:49:30 +00:00
println("Bookmarks: --------------------------")
2020-11-07 23:03:53 +00:00
BookmarksDialog(this, bookmarkDatasource) { bookmark ->
2020-09-11 21:02:54 +00:00
model.request(bookmark.uri)
2020-09-10 21:20:24 +00:00
}.show()
2020-09-07 21:25:36 +00:00
}
2020-08-18 16:17:04 +00:00
R.id.overflow_menu_share -> {
Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, binding.addressEdit.text.toString())
type = "text/plain"
startActivity(Intent.createChooser(this, null))
}
}
R.id.overflow_menu_history -> HistoryDialog.show(this, db.history()) { historyAddress ->
2020-08-20 13:52:24 +00:00
model.request(historyAddress)
2020-08-19 11:33:16 +00:00
}
2020-08-18 20:21:43 +00:00
R.id.overflow_menu_about -> AboutDialog.show(this)
2020-11-09 22:02:23 +00:00
R.id.overflow_menu_settings -> {
startActivity(Intent(this, SettingsActivity::class.java))
}
2020-08-18 16:17:04 +00:00
}
}
}
2020-10-09 14:30:22 +00:00
2020-08-18 16:17:04 +00:00
binding.home.setOnClickListener {
val home = PreferenceManager.getDefaultSharedPreferences(this).getString("home_capsule", Ariane.DEFAULT_HOME_CAPSULE)
2020-08-18 16:17:04 +00:00
model.request(home!!)
}
2020-10-09 14:30:22 +00:00
binding.pullToRefresh.setOnRefreshListener {
val address = binding.addressEdit.text.toString()
RuntimeCache.remove(address)
model.request(address)
}
2020-11-09 01:40:00 +00:00
2020-11-09 19:30:53 +00:00
checkIntentExtras(intent)
}
private fun handleState(state: GemState) {
binding.pullToRefresh.isRefreshing = false
when (state) {
is GemState.AppQuery -> runOnUiThread { showAlert("App backdoor/query not implemented yet") }
is GemState.ResponseInput -> runOnUiThread {
loadingView(false)
InputDialog.show(this, state) { queryAddress ->
model.request(queryAddress)
}
}
is GemState.Requesting -> loadingView(true)
is GemState.NotGeminiRequest -> externalProtocol(state)
is GemState.ResponseError -> showAlert("${GeminiResponse.getCodeString(state.header.code)}: ${state.header.meta}")
is GemState.ResponseGemtext -> renderGemtext(state)
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()
}
}
}
}
2020-11-09 19:30:53 +00:00
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.let{
checkIntentExtras(intent)
}
2020-11-09 01:40:00 +00:00
}
/**
*
* Checks intent to see if Activity was opened to handle selected text
*
*/
2020-11-09 19:30:53 +00:00
private fun checkIntentExtras(intent: Intent) {
//Via ProcessTextActivity from selected text in another app
if(intent.hasExtra("process_text")){
val processText = intent.getStringExtra("process_text")
binding.addressEdit.setText(processText)
model.request(processText ?: "")
return
}
2020-11-09 01:40:00 +00:00
2020-11-09 19:30:53 +00:00
//From clicking a gemini:// address
2020-11-09 01:40:00 +00:00
val uri = intent.data
if(uri != null){
binding.addressEdit.setText(uri.toString())
model.request(uri.toString())
return
}
2020-08-20 13:52:24 +00:00
}
2020-08-18 16:17:04 +00:00
private fun showAlert(message: String) = runOnUiThread{
loadingView(false)
2020-08-18 19:02:16 +00:00
if(message.length > 40){
AlertDialog.Builder(this)
.setMessage(message)
.show()
}else {
Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show()
}
2020-08-18 16:17:04 +00:00
}
private fun externalProtocol(state: GemState.NotGeminiRequest) = runOnUiThread {
2020-08-16 21:30:24 +00:00
loadingView(false)
2020-11-08 22:42:24 +00:00
val browserIntent = Intent(Intent.ACTION_VIEW)
browserIntent.data = Uri.parse(state.uri.toString())
2020-11-07 23:03:53 +00:00
2020-11-08 22:42:24 +00:00
try {
2020-11-07 23:03:53 +00:00
startActivity(browserIntent)
2020-11-08 22:42:24 +00:00
}catch (e: ActivityNotFoundException){
2020-11-07 23:03:53 +00:00
showAlert("No app installed that can open ${state.uri}")
}
}
private fun renderGemtext(state: GemState.ResponseGemtext) = runOnUiThread {
2020-08-16 21:30:24 +00:00
loadingView(false)
//todo - colours didn't change when switching themes, so disabled for now
//val addressSpan = SpannableString(state.uri.toString())
//addressSpan.set(0, 9, ForegroundColorSpan(resources.getColor(R.color.protocol_address)))
binding.addressEdit.setText(state.uri.toString())
2020-11-07 22:58:21 +00:00
2020-08-18 16:17:04 +00:00
adapter.render(state.lines)
2020-08-20 13:52:24 +00:00
2020-11-07 22:58:21 +00:00
//Scroll to top
binding.gemtextRecycler.post {
binding.gemtextRecycler.scrollToPosition(0)
}
2020-08-15 14:52:27 +00:00
}
2020-08-15 20:20:15 +00:00
private fun renderText(state: GemState.ResponseText) = runOnUiThread {
2020-08-20 17:40:54 +00:00
loadingView(false)
TextDialog.show(this, state)
}
var imageState: GemState.ResponseImage? = null
var audioState: GemState.ResponseAudio? = null
2020-11-13 15:38:57 +00:00
var binaryState: GemState.ResponseBinary? = null
2020-08-21 15:46:08 +00:00
private fun renderAudio(state: GemState.ResponseAudio) = runOnUiThread {
2020-08-25 11:57:35 +00:00
loadingView(false)
2020-08-26 16:13:26 +00:00
AudioPlayer.play(this, binding, mediaPlayer, state){ state ->
audioState = state
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "audio/mpeg"
intent.putExtra(Intent.EXTRA_TITLE, File(state.uri.path).name)
startActivityForResult(intent, CREATE_AUDIO_FILE_REQ)
}
2020-08-21 15:59:07 +00:00
}
private fun renderImage(state: GemState.ResponseImage) = runOnUiThread{
2020-08-21 15:12:00 +00:00
loadingView(false)
2020-08-21 15:46:08 +00:00
ImageDialog.show(this, state){ state ->
imageState = state
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
intent.putExtra(Intent.EXTRA_TITLE, File(state.uri.path).name)
startActivityForResult(intent, CREATE_IMAGE_FILE_REQ)
}
}
2020-11-13 15:38:57 +00:00
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)
}
2020-08-21 15:46:08 +00:00
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
2020-11-13 15:38:57 +00:00
if(resultCode == RESULT_OK && (requestCode == CREATE_IMAGE_FILE_REQ || requestCode == CREATE_AUDIO_FILE_REQ || requestCode == CREATE_BINARY_FILE_REQ)){
2020-11-13 15:39:21 +00:00
//todo - tidy this mess up... refactor - none of this should be here
2020-11-13 15:38:57 +00:00
if(imageState == null && audioState == null && binaryState == null) return
2020-08-26 16:13:26 +00:00
data?.data?.let{ uri ->
val cachedFile = when {
2020-11-13 15:38:57 +00:00
imageState != null -> File(imageState!!.cacheUri.path ?: "")
audioState != null -> File(audioState!!.cacheUri.path ?: "")
binaryState != null -> File(binaryState!!.cacheUri.path ?: "")
2020-08-26 16:13:26 +00:00
else -> {
2020-11-13 15:38:57 +00:00
println("File download error - no state object exists")
showAlert("File download error - no state object exists")
null
2020-08-26 16:13:26 +00:00
}
}
2020-11-13 15:38:57 +00:00
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()
}
}
2020-08-21 15:46:08 +00:00
}
}
}
2020-08-26 16:13:26 +00:00
imageState = null
audioState = null
2020-11-13 15:38:57 +00:00
binaryState = null
2020-08-21 15:46:08 +00:00
}
2020-08-21 15:12:00 +00:00
}
2020-08-16 21:30:24 +00:00
private fun loadingView(visible: Boolean) = runOnUiThread {
binding.progressBar.visibleRetainingSpace(visible)
if(visible) binding.appBar.setExpanded(true)
}
2020-08-15 20:20:15 +00:00
override fun onBackPressed() {
if(model.canGoBack()){
model.goBack{ state ->
/*
Passing the callback here so we can eventually add a mechanism to restore scroll position
*/
handleState(state)
}
2020-08-15 20:20:15 +00:00
}else{
2020-11-13 20:51:20 +00:00
println("Ariane history is empty - exiting")
2020-08-15 20:20:15 +00:00
super.onBackPressed()
2020-11-16 19:26:37 +00:00
cacheDir.deleteRecursively()
2020-08-15 20:20:15 +00:00
}
}
2020-08-15 14:52:27 +00:00
}