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.

1148 lines
45 KiB

package org.wntr.mdeditor
import android.app.Activity
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
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 org.wntr.mdeditor.databinding.ActivityMainBinding
import retrofit2.Response
import java.io.BufferedReader
import java.io.File
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 lateinit var binding: ActivityMainBinding
private val easyMDEscript = """
const 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",
}
]
});
"""
companion object {
const val CREATE_FILE = 1
const val OPEN_FILE = 2
const val DELETE_GHOST = 4
var deleteVisible = false
var mdeValue: String = ""
var metaData = mdMeta()
var mdToAppend: String = ""
var thisFileUri: Uri? = null
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.loadUrl("file:///android_res/raw/index.html")
val jsi = object {
@JavascriptInterface
fun getValue(): String {
return mdeValue
}
@JavascriptInterface
fun reportChange(value: String) {
Log.i(javaClass.simpleName, value)
}
@JavascriptInterface
fun triggerSaveFile(value: String) {
mdeValue = value
saveFile()
}
/*@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 refresh() {
readFile(thisFileUri!!)
}
@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 triggerGhost(sharedText: String) {
shareGhost(sharedText, ::sendPost)
}
@JavascriptInterface
fun getMdToAppend(): String {
val md = mdToAppend
mdToAppend = ""
return md
}
@JavascriptInterface
fun getCursor(): String {
val cursor = JSONObject(JSONObject(metaData.cursor), arrayOf("ch", "line"))
Log.i(javaClass.simpleName,"delivering cursor: $cursor")
return cursor.toString()
}
@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()
// 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
}
loadMetaFromSharedPrefs()
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()
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 -> {
saveFile()
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
shareGhost(msg, ::sendPost)
})
})
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)!!
shareGhost(file, ::pushImageFile)
}
fun pushImageFile(username: String?, file: File): retrofit2.Response<Any> {
var response: retrofit2.Response<imagesObj> = retrofit2.Response.error(
444,
"error".toResponseBody("text/plain".toMediaTypeOrNull())
)
try {
response = api.postApi.pushMyImage(
MultipartBody.Part.createFormData(
"file",
file.name,
file.asRequestBody("image/jpeg".toMediaTypeOrNull())
)
).execute()
} catch (e:Exception) {
this.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.runOnUiThread(Runnable() {
Toast.makeText(
this@MainActivity,
"\"${file.name}\" uploaded to \"$imgUrl\"",
Toast.LENGTH_SHORT
).show()
webView.evaluateJavascript("pasteText()") {}
})
return response as retrofit2.Response<Any>
} else return response as retrofit2.Response<Any>
}
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): retrofit2.Response<Any> {
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))
try {
val 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()
val intent = Intent(Intent.ACTION_VIEW).setData(uri)
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, "No eligble app installed.", Toast.LENGTH_LONG).show()
Log.i(javaClass.simpleName, e.toString())
}
} else if (response.code()==403){
this.runOnUiThread({
Toast.makeText(this, "You are not authorized to add posts", Toast.LENGTH_LONG).show()
})
}
return response as Response<Any>
} catch (ex: Exception) {
Log.d(javaClass.simpleName, "Couldn't send posting. Exception: ${ex}")
return retrofit2.Response.error(
444,
"error".toResponseBody("text/plain".toMediaTypeOrNull())
)
}
}
fun sendPost(username: String, text: String): retrofit2.Response<Any> {
return sendPosting(html = text, author = username)
}
fun shareGhost(
text: String,
sendpost: (username: String, text: String) -> retrofit2.Response<Any>
) {
shareGhost(
text = text,
file = null,
sendpost = sendpost as (String?, Any?) -> retrofit2.Response<Any>
)
}
fun shareGhost(
file: File,
sendimage: (username: String, file: File) -> retrofit2.Response<Any>
) {
shareGhost(
text = null,
file,
sendpost = sendimage as (String?, Any?) -> retrofit2.Response<Any>
)
}
fun shareGhost(
text: String?,
file: File?,
sendpost: (username: String?, content: Any?) -> retrofit2.Response<Any>
) {
val instance = credManager.instance
if (instance == "nowhere") {
checkGhostConnection()
return
}
val username = credManager.username
api = ghostAPI(applicationContext, instance)
var result: retrofit2.Response<Any> =
retrofit2.Response.error(444, "error".toResponseBody("text/plain".toMediaTypeOrNull()))
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
if (text !== null) result = sendpost(username, text)
if (file !== null) result = sendpost(null, file)
}
if (text !== null && result.isSuccessful) {
deleteVisible = true
invalidateOptionsMenu()
}
}
}
fun checkGhostConnection(): Boolean {
if (ghostConnection) return true
if (SharedPrefsCookiePersistor(applicationContext).loadAll().size == 0) {
ghostSettings.launch(Intent(this, LoginActivity::class.java))
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()
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 == "") {
Log.d(javaClass.simpleName,"Editor delivered empty content. Don't save.")
return@evaluateJavascript
} else if (it == "null") {
Toast.makeText(
this,
"Problem with file buffer. Please reopen manually.",
Toast.LENGTH_LONG
).show()
openFile()
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()
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
invalidateOptionsMenu()
val apiHost = url.scheme + "://" + url.host
Log.i(javaClass.simpleName, "Starting api controller for: $apiHost")
api = ghostAPI(applicationContext, apiHost)
}
}
}
}
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()
})
}
}
}
private fun saveMetaToSharedPrefs() {
if (thisFileUri == null) return
Log.d(javaClass.simpleName, "saving to shared prefs cursor: ${metaData.cursor} in file: ${thisFileUri}")
getSharedPreferences("prefs", Context.MODE_PRIVATE)
.edit().apply {
putString("lastFile", thisFileUri.toString())
putString("cursor", metaData.cursor)
apply()
}
}
private fun loadMetaFromSharedPrefs(){
val prefs = getSharedPreferences("prefs", Context.MODE_PRIVATE)
val uriString = prefs.getString("lastFile", "noLastFile")
if (uriString == "noLastFile") {
with(AlertDialog.Builder(this)){
setPositiveButton("Open", { dialog, id ->
openFile()
})
setNeutralButton("New", { dialog, id ->
selectFileForSaveAs()
})
show()
}
} else {
thisFileUri = parse(uriString)
metaData.cursor = prefs.getString("cursor", "nocursor") ?: "{ line: 0, ch: 0, sticky: null }"
Log.i(javaClass.simpleName,"Loaded cursor: ${metaData.cursor}")
}
}
override fun onPause() {
super.onPause()
try{
autosaveTimer.cancel()
} catch (e:Exception) {
Log.d (javaClass.simpleName, "Couldn't cancel autosaveTimer.\n$e")
}
saveFile()
webView.evaluateJavascript("easyMDE.codemirror.doc.getCursor();") {
metaData.cursor=it
Log.i(javaClass.simpleName,"Cursor: $it")
saveMetaToSharedPrefs()
}
Log.i(javaClass.simpleName, "\"onPause\" durchlaufen")
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
Log.i(javaClass.simpleName, "\"onRestoreInstanceState\" durchlaufen")
}
}