You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1078 lines
44 KiB

package org.wntr.mdeditor
import android.app.Activity
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.net.Uri.parse
import android.os.Build
import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.WindowInsets
import android.view.WindowManager
import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import org.json.JSONObject
import retrofit2.Response
import java.io.BufferedReader
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStreamReader
import java.lang.Thread.sleep
import java.net.URLDecoder
import java.time.Instant
import java.util.Timer
import kotlin.concurrent.fixedRateTimer
class MainActivity : AppCompatActivity() {
private val easyMDEscript = """
if (typeof easyMDE != 'undefined') throw new Error("easyMDE already loaded");
easyMDE = new EasyMDE({
spellChecker: false,
nativeSpellcheck: false,
maxHeight: String(windowHeight-120)+"px",
inputStyle: "textarea",
autoDownloadFontAwesome: false,
theme: "solarized",
status: [
{
className: "editor-statusbar-left",
onUpdate: (el) => {
el.innerHTML = saveStatus()
}
},
{
className: "displayName",
defaultValue: "None",
onUpdate: (el) => {
el.innerHTML = displayName()
},
}, "lines", "words", "cursor",
{
className: "editor-statusbar-right",
onUpdate: (el) => {
el.innerHTML = "<i class=\"fa fa-square\"></i>"
}
}
],
toolbar: [
{
name: "toggleTheme",
action: toggleTheme,
className: "fa fa-moon",
title: "Toggle Theme"
},
{
name: "share",
action: shareText,
className: "fa fa-share-nodes",
title: "Share"
},"strikethrough", "horizontal-rule","undo",
{
name: "preview",
action: myPreview,
className: "fa fa-eye",
title: "Preview",
noDisable: true
},"redo",
"bold", "italic","link","code",
{
name: "toggle",
action: toggleBar,
className: "fa fa-expand",
title: "Toggle Bar",
}
]
});
easyMDE.codemirror.getScrollerElement().style.minHeight="100px";
"""
companion object {
const val CREATE_FILE = 1
const val OPEN_FILE = 2
var deleteVisible = false
var mdeValue: String = ""
var mdToAppend: String = ""
var truncate = false
lateinit var pickMultipleVisualMedia: ActivityResultLauncher<PickVisualMediaRequest>
lateinit var ghostSettings: ActivityResultLauncher<Intent>
lateinit var ghostMetaData: ActivityResultLauncher<Intent>
lateinit var webView: WebView
lateinit var api: ghostAPI
lateinit var autosaveTimer: Timer
var ghostConnection = false
lateinit var credManager: CredentialManager
var intentScheme = "none"
var easyMDELoaded = false
var readOnResume = true
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportActionBar!!.setDisplayShowTitleEnabled(false)
deleteCache(applicationContext)
Log.i(javaClass.simpleName, "intent data on start: ${intent.data.toString()}\nIntent action: ${intent.action}")
credManager = CredentialManager(applicationContext)
webView = findViewById<WebView>(R.id.mde_webview)
webView.settings.javaScriptEnabled = true
webView.setLongClickable(true);
webView.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
Log.d("WebView", consoleMessage.message())
return true
}
override fun onProgressChanged(view: WebView?, newProgress: Int) {
super.onProgressChanged(view, newProgress)
Log.d(javaClass.simpleName, "new progress: ${newProgress}")
if (newProgress == 100) {
webView.evaluateJavascript(easyMDEscript, {
easyMDELoaded = true
Log.d(javaClass.simpleName, "easyMDE loaded")
})
}
}
}
webView.viewTreeObserver.addOnGlobalLayoutListener {
webView.evaluateJavascript("easyMDE.codemirror.getScrollerElement().style.height=String(window.innerHeight-120) +\"px\"", {
Log.d(javaClass.simpleName, "js window innerheight set to: $it")
})
}
webView.loadUrl("file:///android_res/raw/index.html")
val jsi = object {
/*@JavascriptInterface
fun getHeight(): Int {
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
val height = displayMetrics.heightPixels
Log.i(javaClass.simpleName, "display is $height pixels high.")
return height
}*/
@JavascriptInterface
fun triggerDisplayName(): String {
return getDisplayName(applicationContext, thisFileUri)
}
@JavascriptInterface
fun triggerShare(sharedText: String) {
val htmlFile = createHtmlFile(applicationContext, sharedText)
if (htmlFile == null) return
val htmlUri = FileProvider.getUriForFile(applicationContext, "org.wntr.mdeditor.fileprovider", htmlFile)
with(Intent(Intent.ACTION_SEND)) {
htmlFile?.also {
type = "text/plain"
putExtra(Intent.EXTRA_STREAM, htmlUri)
startActivity(Intent.createChooser(this, "Share html"))
}
}
}
@JavascriptInterface
fun getMdToAppend(): String {
val md = mdToAppend
mdToAppend = ""
return md
}
@JavascriptInterface
fun isFullscreen() : Boolean{
return supportActionBar!!.isShowing
}
@JavascriptInterface
fun toggleBar() {
this@MainActivity.runOnUiThread({
if (supportActionBar!!.isShowing) {
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.hide(WindowInsets.Type.statusBars())
} else {
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
0
)
}
supportActionBar!!.hide()
}
else {
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.show(WindowInsets.Type.statusBars())
} else {
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
}
supportActionBar!!.show()
}
})
}
}
webView.addJavascriptInterface(jsi, "Android")
pickMultipleVisualMedia = registerForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(
5
)
) { uris ->
// Process URIs
Toast.makeText(
this@MainActivity,
"Uploading\n$uris",
Toast.LENGTH_LONG
).show()
// TODO: This check for connection needs to be refined once multiple accounts are supported
checkGhostConnection()
for (uri in uris) pushImage(uri)
Log.d(javaClass.simpleName, "Photo Picker URIs count ${uris.size}")
}
ghostSettings = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
checkGhostConnection()
}
ghostMetaData = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
Log.d(javaClass.simpleName,"Saving new Metadata to file")
saveFile()
}
}
override fun onResume() {
super.onResume()
if (!readOnResume) {
readOnResume = true
autosaveTimer = fixedRateTimer("timer",true,0,5000){
this@MainActivity.runOnUiThread {
val script = "easyMDE.codemirror.doc.isClean();"
webView.evaluateJavascript(script , {
if (it == "false" && thisFileUri != null) {
saveFile()
}
})
}
}
Log.d(javaClass.simpleName, "AutosaveTimer started.")
return
}
if (!loadMetaFromSharedPrefs(applicationContext)) {
with(AlertDialog.Builder(this)){
setPositiveButton("Open", { dialog, id ->
openFile()
})
setNeutralButton("New", { dialog, id ->
selectFileForSaveAs()
})
show()
}
}
if (intent.data !== null) {
readFile(intent.data!!)
Log.d(javaClass.simpleName,"Loading on resume from intent: ${thisFileUri}")
intent.data = null
} else if (thisFileUri !== null ) {
readFile(thisFileUri!!)
Log.d(javaClass.simpleName,"Loading on resume from thisFileUri: ${thisFileUri}")
}
}
override fun onNewIntent(intent:Intent) {
super.onNewIntent(intent)
Log.i(javaClass.simpleName, "intent data onNewIntent: ${intent.data.toString()}\nIntent action: ${intent.action}")
var uri:Uri? = null
var mimeType: String? = null
if (intent.action == Intent.ACTION_SEND && intent.extras != null) {
if (intent.extras!!.get(Intent.EXTRA_STREAM) != null){
uri = intent.extras!!.get(Intent.EXTRA_STREAM) as Uri
mimeType = "image"
} else {
uri = parse(intent.extras!!.getCharSequence(Intent.EXTRA_TEXT).toString())
}
} else if (intent.action == Intent.ACTION_VIEW && intent.data != null) {
uri = parse(intent.dataString)
mimeType = contentResolver.getType(uri)
if (mimeType != null) {
if (mimeType.split("/")[0] == "text" ) {
Log.i(javaClass.simpleName, "Wanna open a text file")
mimeType = "text"
}
else if (mimeType.split("/")[0] == "image" ) {
Log.i(javaClass.simpleName, "Wanna upload a image")
mimeType = "image"
}
}
}
Log.i(javaClass.simpleName,"Got a new intent with URI: $uri")
if (uri != null && uri.toString()!="null") {
intentScheme = uri.scheme!!
if (intentScheme == "content") {
Log.i(javaClass.simpleName, "content intent")
}
else if (intentScheme =="http" || intentScheme == "https") {
intentScheme = "link"
Log.i(javaClass.simpleName, "link intent")
}
else Log.i(javaClass.simpleName, "unknown intent")
}
if (intentScheme == "link" && uri.toString() !="null") {
mdToAppend += "[](${uri})\n"
intent.putExtra(Intent.EXTRA_TEXT, null as CharSequence?)
}
if (intentScheme == "content") {
if (mimeType == "image" && uri != null) {
pushImage(uri)
intent.putExtra(Intent.EXTRA_STREAM, null as Uri?)
} else if (mimeType == "text" && intent.data != null ) {
thisFileUri = uri
saveMetaToSharedPrefs(applicationContext)
Log.d(javaClass.simpleName,"wanna start app with new txt")
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
Log.d(javaClass.simpleName,"create options menu")
menuInflater.inflate(R.menu.ghost_menu, menu)
return true
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
menu.findItem(R.id.delete_ghost).setVisible(deleteVisible)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
when (id) {
R.id.new_file -> {
truncate = true
selectFileForSaveAs()
}
R.id.open_file -> {
openFile()
}
R.id.save_file -> {
selectFileForSaveAs()
}
R.id.push_ghost -> {
with(AlertDialog.Builder(this)){
if (metaData.get("url") != null) setTitle("Update ghost posting to ${metaData.get("url")}?")
else setTitle("Push to ghost instance ${credManager.instance}?")
setPositiveButton("Yes", { dialog, id ->
webView.evaluateJavascript("getHtml();", {
val msg = URLDecoder.decode(it.removeSurrounding("\""))
if (metaData.metaData.get("url") !== null) {
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
if (metaData.getId() == null) {
this@MainActivity.runOnUiThread({
Toast.makeText(
this@MainActivity,
"No ID for url in Metadata found. Deleted? Trying repost.",
Toast.LENGTH_LONG
).show()
})
sendPosting(html=msg, author = credManager.username)
} else {
Log.i(javaClass.simpleName,"posting $msg")
updatePost(
title = metaData.get("title") ?: getDisplayName(applicationContext, thisFileUri),
author = credManager.username,
html = msg,
id = metaData.getId()!!
)
}
}
}
} else
sendPosting(html=msg, author = credManager.username)
})
})
setNeutralButton("No", { dialog, id -> })
show()
}
}
R.id.settings -> {
ghostSettings.launch(Intent(this, LoginActivity::class.java))
}
R.id.image -> {
pickMultipleVisualMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo))
}
R.id.metadata -> {
ghostMetaData.launch(Intent(this, MetadataActivity::class.java))
}
R.id.delete_ghost -> {
checkGhostConnection()
var response: retrofit2.Response<ResponseBody> = retrofit2.Response.error(
444,
"error".toResponseBody("text/plain".toMediaTypeOrNull())
)
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
if (metaData.ID == null) {
if (metaData.getId() == null) {
// No ID for URL found
saveFile()
deleteVisible = false
invalidateOptionsMenu()
return@withContext false
}
}
try {
response = api.postApi.deletePost(metaData.ID!!).execute()
} catch (e:Exception) {
Log.d(javaClass.simpleName, "Exception during DELETE: $e")
}
Log.d(javaClass.simpleName, "result: ${response.code()}")
}
if (response.isSuccessful) {
this@MainActivity.runOnUiThread({
Toast.makeText(
this@MainActivity,
"Post under ${metaData.metaData.get("url")} got deleted.",
Toast.LENGTH_LONG
).show()
})
Log.d(javaClass.simpleName, "Post under ${metaData.metaData.get("url")} got deleted.")
metaData.metaData.minusAssign("url")
metaData.ID=null
deleteVisible = false
invalidateOptionsMenu()
saveFile()
}
}
}
}
return true
}
fun pushImage(uri: Uri) {
// see https://androidfortechs.blogspot.com/2020/12/how-to-convert-uri-to-file-android-10.html
val file = getFile(applicationContext, uri)!!
val instance = credManager.instance
if (instance == "nowhere") {
checkGhostConnection()
return
}
api = ghostAPI(applicationContext, instance)
var response: retrofit2.Response<imagesObj> = retrofit2.Response.error(
444,
"error".toResponseBody("text/plain".toMediaTypeOrNull())
)
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
try {
response = api.postApi.pushMyImage(
MultipartBody.Part.createFormData(
"file",
file.name,
file.asRequestBody("image/jpeg".toMediaTypeOrNull())
)
).execute()
} catch (e: Exception) {
this@MainActivity.runOnUiThread(Runnable() {
Toast.makeText(
this@MainActivity,
"Exception during image upload: $e",
Toast.LENGTH_SHORT
).show()
})
Log.i(javaClass.simpleName, "Exception during image upload:\n$e")
}
if (response.isSuccessful) {
val imgUrl = response.body()!!.images[0].url
Log.d(javaClass.simpleName, "\"${file.name}\" uploaded to \"$imgUrl\"")
mdToAppend += "![${file.name}]($imgUrl)\n"
this@MainActivity.runOnUiThread(Runnable() {
Toast.makeText(
this@MainActivity,
"\"${file.name}\" uploaded to \"$imgUrl\"",
Toast.LENGTH_SHORT
).show()
webView.evaluateJavascript("pasteText()") {}
})
}
}
}
}
fun updatePost(title: String, html: String, author: String, id: String) : retrofit2.Response<Any> {
val post = sendPost(title, updated_at = metaData.updatedAt!!, authors = listOf(author), html, feature_image = metaData.get("feature_image"))
val postings = sendPostList(listOf(post))
val mUrl = parse(metaData.get("url"))
val mApiHost = mUrl.scheme + "://" + mUrl.host
if (api.baseUrl != mApiHost) api = ghostAPI(applicationContext, mApiHost)
credManager.saveCredentialsToSharedPrefs(Credentials(mApiHost,author))
Log.d(javaClass.simpleName,"Updating post to ${mUrl}")
try {
val response = api.postApi.updatePost(id, postings).execute()
if (response.isSuccessful) {
val resp = JSONObject(response.body()!!.string())
Log.d(javaClass.simpleName, "Updated: ${resp.getJSONArray("posts").getJSONObject(0).getString("url")}")
metaData.updatedAt= resp.getJSONArray("posts").getJSONObject(0).getString("updated_at")
val intent = Intent(Intent.ACTION_VIEW).setData(parse(metaData.metaData.get("url")))
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
this.runOnUiThread({
Toast.makeText(this, "No eligble app installed.", Toast.LENGTH_LONG).show()
Log.i(javaClass.simpleName, e.toString())
})
}
} else {
this.runOnUiThread({
Toast.makeText(
this,
"error while updating post.\n${response.errorBody()!!.string()}",
Toast.LENGTH_LONG
).show()
})
Log.i(javaClass.simpleName, response.errorBody()!!.string())
}
return response as retrofit2.Response<Any>
} catch (ex: Exception) {
Log.d(javaClass.simpleName, "Couldn't update posting. Exception: ${ex}")
return retrofit2.Response.error(
444,
"error".toResponseBody("text/plain".toMediaTypeOrNull())
)
}
}
fun sendPosting(html: String, author: String){
val title = metaData.get("title") ?: getDisplayName(applicationContext, thisFileUri)
val post = sendPost(title, updated_at = Instant.now().toString(), authors = listOf(author), html, feature_image = metaData.get("feature_image"))
val postings = sendPostList(listOf(post))
lateinit var response: Response<ResponseBody>
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
try {
val instance = credManager.instance
if (instance == "nowhere") {
checkGhostConnection()
return@withContext
}
api = ghostAPI(applicationContext, instance)
response = api.postApi.pushPost(postings).execute()
Log.d(javaClass.simpleName, "result: ${response.code()}")
if (response.isSuccessful) {
val resp = JSONObject(response.body()!!.string())
val post = resp.getJSONArray("posts").getJSONObject(0)
val uri = parse(post.getString("url"))
metaData.ID = post.getString("id")
metaData.updatedAt = post.getString("updated_at")
Log.d(javaClass.simpleName, "Uploaded to: $uri\nID: ${metaData.ID}")
metaData.put("url", post.getString("url"))
saveFile()
deleteVisible = true
invalidateOptionsMenu()
val intent = Intent(Intent.ACTION_VIEW).setData(uri)
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
this@MainActivity.runOnUiThread({
Toast.makeText(
this@MainActivity,
"No eligble app installed.",
Toast.LENGTH_LONG
)
.show()
Log.i(javaClass.simpleName, e.toString())
})
}
} else if (response.code() == 403) {
this@MainActivity.runOnUiThread({
Toast.makeText(
this@MainActivity,
"You are not authorized to add posts",
Toast.LENGTH_LONG
).show()
})
}
} catch (ex: Exception) {
Log.d(javaClass.simpleName, "Couldn't send posting. Exception: ${ex}")
return@withContext
}
}
}
}
fun checkGhostConnection(): Boolean {
if (ghostConnection) return true
if (SharedPrefsCookiePersistor(applicationContext).loadAll().size == 0) {
this.runOnUiThread({
with(AlertDialog.Builder(this)){
setTitle("No ghost CMS login defined. Edit credentials?")
setPositiveButton("Yes", { dialog, id ->
ghostSettings.launch(Intent(this@MainActivity, LoginActivity::class.java))
})
setNeutralButton("No", { dialog, id ->
})
show()
}
})
return false
}
// we have a cookie
return true
}
fun openFile() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(DocumentsContract.EXTRA_INITIAL_URI, thisFileUri)
type = "text/*"
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
startActivityForResult(intent, OPEN_FILE)
}
fun selectFileForSaveAs() {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/*"
putExtra(Intent.EXTRA_TITLE, getDisplayName(applicationContext, thisFileUri))
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
this.runOnUiThread({
Toast.makeText(
this,
"Please select a location for the new file.",
Toast.LENGTH_LONG
).show()
})
startActivityForResult(intent, CREATE_FILE)
Log.i(javaClass.simpleName, "Buffer geschrieben")
}
/* Cut is not possible currently because of the deactivation of the custom text selection and
context menu mechanisms.
override fun onActionModeStarted(mode: ActionMode?) {
super.onActionModeStarted(mode)
val myContextMenu = mode!!.menu
myContextMenu.add(NONE, CONTEXT_MENU_CUT, NONE,"Cut" )
myContextMenu.getItem(0).setOnMenuItemClickListener {
Log.i(javaClass.simpleName,"somwhere in between")
when(it.itemId) {
CONTEXT_MENU_CUT -> {
webView.evaluateJavascript("dispatchCut();", ValueCallback<String>() {})
true
}
else -> {false}
}
}
}
*/
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData)
if (requestCode == CREATE_FILE && resultCode == Activity.RESULT_OK) {
// The result data contains a URI for the document or directory that
// the user selected.
resultData?.data?.also { uri ->
getContentResolver().takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
Log.d(javaClass.simpleName, "Saving to: ${getDisplayName(applicationContext, uri)}")
if (getDisplayName(applicationContext, thisFileUri) + " (1)" != getDisplayName(applicationContext, uri)) thisFileUri = uri
saveAs()
}
} else if (requestCode == OPEN_FILE && resultCode == Activity.RESULT_OK) {
resultData?.data?.also { uri ->
getContentResolver().takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION and Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
thisFileUri = uri
saveMetaToSharedPrefs(applicationContext)
if (metaData.metaData.get("url") == null) {
deleteVisible = false
invalidateOptionsMenu()
} else {
deleteVisible = true
invalidateOptionsMenu()
}
}
}
}
fun saveFile() {
if (thisFileUri == null) {
Log.d(javaClass.simpleName, "File Uri got null. Can't save.")
return
}
lateinit var textFile: ParcelFileDescriptor
try {
textFile = contentResolver.openFileDescriptor(thisFileUri!!, "w")!!
textFile.checkError()
} catch (e: java.lang.IllegalArgumentException) {
Log.d(javaClass.simpleName, "Problem with saving file\nBug in Android 11?\n${e.stackTraceToString()}")
this.runOnUiThread(
{
Toast.makeText(
this,
"Problem with saving file\n" +
"Bug in Android 11?\n" +
"Workaround: Load file with normal file selection",
Toast.LENGTH_LONG
).show()
}
)
return
// TODO: implement workaround for this bug. See: https://stackoverflow.com/q/69248596
} catch (e: java.io.FileNotFoundException) {
Log.d(javaClass.simpleName, "File not found. Deleted while loaded?\n${e.stackTraceToString()}")
this.runOnUiThread(
{
Toast.makeText(
this,
"File not found. Deleted while loaded?",
Toast.LENGTH_LONG
).show()
}
)
selectFileForSaveAs()
return
}
catch (e: Exception) {
Log.d(javaClass.simpleName, "Problem with accessing file\n${e.stackTraceToString()}")
this.runOnUiThread(
{
Toast.makeText(
this,
"Problem with accessing file\n$e",
Toast.LENGTH_LONG
).show()
}
)
return
}
try {
this.runOnUiThread({
webView.evaluateJavascript("getValue();") {
if (it.equals("") || it.equals("\"\"") || it.equals("\"null\"") || it.equals("null")) {
Log.d(javaClass.simpleName,"Editor delivered empty content. No save.")
Toast.makeText(
this,
"Editor delivered empty content. Didn't save. Please reopen manually.",
Toast.LENGTH_LONG
).show()
return@evaluateJavascript
}
mdeValue =
metaData.toString() + URLDecoder.decode(it.removeSurrounding("\""))
if (mdeValue.length.toLong() == textFile.statSize) {
Log.d(javaClass.simpleName, "No change on disk, file not saved.\n$mdeValue")
return@evaluateJavascript
}
contentResolver.openFileDescriptor(thisFileUri!!, "wt")?.use {
FileOutputStream(it.fileDescriptor).use {
it.write(mdeValue.toByteArray())
}
}
Log.d(javaClass.simpleName, "File saved: ${thisFileUri}")
textFile.close()
this@MainActivity.runOnUiThread {
webView.evaluateJavascript(
"easyMDE.codemirror.doc.markClean();" +
"easyMDE.updateStatusBar(\"editor-statusbar-left\",saveStatus());"
, {})
}
}
})
} catch (e: Exception) {
Toast.makeText(
this,
"Error during writing.\n$e",
Toast.LENGTH_LONG
).show()
return
}
}
private fun saveAs() {
try {
lateinit var textFile: ParcelFileDescriptor
try {
textFile = contentResolver.openFileDescriptor(thisFileUri!!, "w")!!
textFile.checkError()
} catch (e: Exception) {
Toast.makeText(
this,
"Problem with accessing file\n$e",
Toast.LENGTH_LONG
).show()
return
}
if (truncate) {
mdeValue = ""
truncate = false
}
FileOutputStream(textFile.fileDescriptor).use {
it.write(mdeValue.toByteArray())
}
Toast.makeText(
this,
"File saved.",
Toast.LENGTH_LONG
).show()
textFile.close()
saveMetaToSharedPrefs(applicationContext)
Log.i(javaClass.simpleName, "file newly written")
} catch (e: Exception) {
Toast.makeText(
this,
"Error during writing.\n$e",
Toast.LENGTH_LONG
).show()
}
}
@Throws(IOException::class)
private fun readFile(uri: Uri) {
try{
autosaveTimer.cancel()
Log.d(javaClass.simpleName, "Stopped autosaveTimer.")
} catch (e:UninitializedPropertyAccessException) {
Log.d (javaClass.simpleName, "Couldn't cancel autosaveTimer. Not yet initialized")
}
//TODO: Which permissions are really needed?
try {
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
} catch (e: java.lang.SecurityException) {
Log.d(javaClass.simpleName, "Couldn't get persistable URI permissions. Trying to grant 'em.\n${e.stackTraceToString()}")
}
try {
grantUriPermission(
"org.wntr.mdeditor",
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
} catch (e: java.lang.SecurityException) {
Log.d(javaClass.simpleName, e.stackTraceToString())
this.runOnUiThread({
Toast.makeText(
this,
"Security exception during file load. Try open manually.",
Toast.LENGTH_LONG
).show()
})
}
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
try {
if (!checkURIResource(applicationContext, uri)) {
Log.d(javaClass.simpleName, "URI unavailable")
this@MainActivity.runOnUiThread({
Toast.makeText(
this@MainActivity,
"URI check failed\n" +
"Better open from another storage location\n" ,
Toast.LENGTH_LONG
).show()
})
thisFileUri = null
return@withContext
}
contentResolver.openInputStream(uri)?.use { inputStream ->
BufferedReader(InputStreamReader(inputStream)).use { reader ->
var i = ""
if (reader.ready()) {
i = reader.readLine()
var line: String? = ""
while (line != null) {
i += line + '\n'
line = reader.readLine()
}
}
reader.close()
mdeValue = metaData.extractMetadataFromMarkdown(i)
if (metaData.get("url") !== null) {
val url = parse(metaData.get("url"))
deleteVisible = true
val apiHost = url.scheme + "://" + url.host
Log.i(javaClass.simpleName, "Starting api controller for: $apiHost")
api = ghostAPI(applicationContext, apiHost)
} else {
deleteVisible = false
}
invalidateOptionsMenu()
}
}
}
catch (e: java.io.FileNotFoundException) {
Log.d(javaClass.simpleName, "File not found during reading:\n${e.stackTraceToString()}")
this@MainActivity.runOnUiThread({
Toast.makeText(
this@MainActivity,
"File not found during reading.\n" +
"Current Buffer is not connected to any file now.\n" +
"Save with other file name or open another.",
Toast.LENGTH_LONG
).show()
})
thisFileUri = null
return@withContext
}
catch (e: java.lang.NullPointerException) {
Log.d(javaClass.simpleName, "Nullpointerexception. Maybe file deleted in the meantime?.\n$e")
this@MainActivity.runOnUiThread({
Toast.makeText(
this@MainActivity,
"Nullpointerexception. Maybe file deleted in the meantime?.\n$e",
Toast.LENGTH_LONG
).show()
})
thisFileUri = null
return@withContext
}
catch (e: Exception) {
Log.d(javaClass.simpleName, e.stackTraceToString())
this@MainActivity.runOnUiThread({
Toast.makeText(
this@MainActivity,
"Error during reading.\n$e",
Toast.LENGTH_LONG
).show()
})
thisFileUri = null
return@withContext
}
while (!easyMDELoaded) {
sleep(100)
Log.d(javaClass.simpleName,"waiting for easyMDE")
}
this@MainActivity.runOnUiThread({
val script = "if (typeof easyMDE !== 'undefined') {" +
"easyMDE.codemirror.doc.setValue(`${mdeValue}`);" +
"easyMDE.codemirror.doc.markClean();" +
"easyMDE.updateStatusBar(\"editor-statusbar-left\",saveStatus());" +
"easyMDE.codemirror.focus();" +
"easyMDE.codemirror.doc.setCursor(JSON.parse(`${metaData.cursor}`));" +
"pasteText();" +
"easyMDE.updateStatusBar(\"displayName\",\"${getDisplayName(applicationContext, uri)}\");}"
Log.d(javaClass.simpleName, "executing in webview:\n${script}")
webView.evaluateJavascript(script, {
thisFileUri = uri
Log.d(javaClass.simpleName,"File read: ${thisFileUri}")
autosaveTimer = fixedRateTimer("timer",true,0,5000){
this@MainActivity.runOnUiThread {
val script = "easyMDE.codemirror.doc.isClean();"
webView.evaluateJavascript(script , {
if (it == "false" && thisFileUri != null) {
saveFile()
}
})
}
}
Log.d(javaClass.simpleName, "AutosaveTimer started.")
})
webView.requestFocus()
})
}
}
}
override fun onPause() {
super.onPause()
try{
autosaveTimer.cancel()
} catch (e:Exception) {
Log.d (javaClass.simpleName, "Couldn't cancel autosaveTimer.\n$e")
}
saveFile()
try {
webView.evaluateJavascript("easyMDE.codemirror.doc.getCursor();") {
metaData.cursor = it
Log.i(javaClass.simpleName, "Cursor: $it")
saveMetaToSharedPrefs(applicationContext)
}
} catch (e: UninitializedPropertyAccessException) {
Log.d(javaClass.simpleName, "Webview not yet loaded.\n$e")
}
easyMDELoaded = false
Log.i(javaClass.simpleName, "\"onPause\" durchlaufen")
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
Log.i(javaClass.simpleName, "\"onRestoreInstanceState\" durchlaufen")
}
}