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.

1197 lines
44 KiB

package org.wntr.mdeditor
import android.annotation.SuppressLint
import android.app.Activity
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.provider.OpenableColumns
import android.util.DisplayMetrics
import android.util.Log
import android.view.Menu
import android.view.Menu.NONE
import android.view.MenuItem
import android.view.WindowInsets
import android.view.WindowManager
import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
import android.webkit.ValueCallback
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 java.io.BufferedReader
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.lang.Thread.sleep
import java.net.URLDecoder
import java.time.Instant
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 = "<i class=\"fa fa-circle\"></i>"
}
},
{
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 tempFile: File
lateinit var pickMultipleVisualMedia: ActivityResultLauncher<PickVisualMediaRequest>
lateinit var ghostSettings: ActivityResultLauncher<Intent>
lateinit var ghostMetaData: ActivityResultLauncher<Intent>
lateinit var webView: WebView
lateinit var api: ghostAPI
var ghostConnection = false
lateinit var credManager: CredentialManager
var intentScheme = "none"
var easyMDELoaded = false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar)) // deprecated source
supportActionBar!!.setDisplayShowTitleEnabled(false)
deleteCache()
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 triggerNewBuffer(value: String) {
saveFile()
mdeValue = value
metaData = mdMeta()
selectFileForSaveAs()
}
@JavascriptInterface
fun triggerOpenFile() {
openFile()
}
@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(thisFileUri)
}
@JavascriptInterface
fun triggerShare(sharedText: String) {
shareHtml(sharedText)
}
@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()) {
saveFile()
}
fixedRateTimer("timer",true,0,5000){
this@MainActivity.runOnUiThread {
val script = "easyMDE.codemirror.doc.isClean();"
webView.evaluateJavascript(script , {
if (it == "false" && thisFileUri != null) {
saveFile()
this@MainActivity.runOnUiThread {
webView.evaluateJavascript("easyMDE.codemirror.doc.markClean();", {})
}
}
})
}
}
}
override fun onResume() {
super.onResume()
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}")
}
if (metaData.metaData.get("url") !== null) {
val url = parse(metaData.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)
}
}
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 {
menuInflater.inflate(R.menu.ghost_menu, menu)
return true
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
if (deleteVisible) {if (menu.findItem(DELETE_GHOST) == null) menu.add(NONE,DELETE_GHOST, NONE,"Delete" )}
else menu.removeItem(DELETE_GHOST)
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 -> {
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(thisFileUri),
author = credManager.username,
html = msg,
id = metaData.getId()!!
)
}
}
}
} else
shareGhost(msg, ::sendPost)
})
}
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))
}
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) {
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
}
@Throws(IOException::class)
fun getFile(context: Context, uri: Uri): File? {
val destinationFilename =
File(context.filesDir.path + File.separatorChar + queryName(context, uri))
try {
context.contentResolver.openInputStream(uri!!).use { ins ->
createFileFromStream(
ins!!,
destinationFilename
)
}
} catch (ex: java.lang.Exception) {
Log.e("Save File", ex.message!!)
ex.printStackTrace()
}
return destinationFilename
}
fun createFileFromStream(ins: InputStream, destination: File?) {
try {
FileOutputStream(destination).use { os ->
val buffer = ByteArray(4096)
var length: Int
while (ins.read(buffer).also { length = it } > 0) {
os.write(buffer, 0, length)
}
os.flush()
}
} catch (ex: java.lang.Exception) {
Log.e("Save File", ex.message!!)
ex.printStackTrace()
}
}
private fun queryName(context: Context, uri: Uri): String? {
val returnCursor = context.contentResolver.query(uri, null, null, null, null)!!
val nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
returnCursor.moveToFirst()
val name = returnCursor.getString(nameIndex)
returnCursor.close()
return name
}
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))
var response: retrofit2.Response<posts> = retrofit2.Response.error(
444,
"error".toResponseBody("text/plain".toMediaTypeOrNull())
)
try {
response = api.postApi.updatePost(id, postings).execute()
} catch (ex: Exception) {
Log.d(javaClass.simpleName, "Couldn't update posting. Exception: ${ex}")
}
if (response.isSuccessful) {
Log.d(javaClass.simpleName, "Updated: ${response.body()!!.posts[0].url}")
metaData.updatedAt= response.body()!!.posts[0].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()}",
Toast.LENGTH_LONG
).show()
})
Log.i(javaClass.simpleName, response.code().toString())
}
return response as retrofit2.Response<Any>
}
fun sendPosting(html: String, author: String): retrofit2.Response<Any> {
val title = metaData.get("title") ?: "test"
val post = sendPost(title, updated_at = Instant.now().toString(), authors = listOf(author), html, feature_image = metaData.get("feature_image"))
val postings = sendPostList(listOf(post))
var response: retrofit2.Response<posts> = retrofit2.Response.error(
444,
"error".toResponseBody("text/plain".toMediaTypeOrNull())
)
try {
response = api.postApi.pushPost(postings).execute()
} catch (ex: Exception) {
Log.d(javaClass.simpleName, "Couldn't send posting. Exception: ${ex}")
}
Log.d(javaClass.simpleName, "result: ${response.body()}")
if (response.isSuccessful) {
val post = response.body()!!.posts[0]
val uri = parse(post.url)
metaData.ID = post.id
metaData.updatedAt = post.updated_at
Log.d(javaClass.simpleName, "Uploaded to: $uri\nID: ${metaData.ID}")
metaData.put("url", uri.toString())
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())
}
}
return response as retrofit2.Response<Any>
}
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 deleteCache() {
try {
val dir = File(cacheDir, "html")
deleteDir(dir)
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
fun deleteDir(dir: File?): Boolean {
return if (dir != null && dir.isDirectory) {
val children = dir.list()
for (i in children.indices) {
val success = deleteDir(File(dir, children[i]))
if (!success) {
return false
}
}
dir.delete()
} else if (dir != null && dir.isFile) {
dir.delete()
} else {
false
}
}
fun shareHtml(htmlString: String): Boolean {
try {
createHTMLFile()
} catch (e: Exception) {
Log.d(javaClass.simpleName, "Problem with accessing file\n${e.stackTraceToString()}")
Toast.makeText(
this,
"Problem with accessing file\n$e",
Toast.LENGTH_LONG
).show()
return false
}
try {
FileOutputStream(tempFile).use {
it.write(htmlString.toByteArray())
}
} catch (e: Exception) {
Toast.makeText(
this,
"Error during writing.\n$e",
Toast.LENGTH_LONG
).show()
return false
}
Log.d(javaClass.simpleName, "File saved: ${tempFile.toURI()}")
Toast.makeText(
this,
"HTML output produced.",
Toast.LENGTH_LONG
).show()
val htmlUri = FileProvider.getUriForFile(this, "org.wntr.mdeditor.fileprovider", tempFile)
with(Intent(Intent.ACTION_SEND)) {
tempFile?.also {
type = "text/plain"
putExtra(Intent.EXTRA_STREAM, htmlUri)
startActivity(Intent.createChooser(this, "Share html"))
}
}
return true
}
@Throws(IOException::class)
private fun createHTMLFile() {
// Create an image file name
val storageDir = File(cacheDir, "html")
storageDir.mkdir()
tempFile = File(storageDir.path + "/${getDisplayName(thisFileUri).split(".")[0]}.html")
if (tempFile.exists()) tempFile.delete()
tempFile.createNewFile()
}
fun openFile() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(DocumentsContract.EXTRA_INITIAL_URI, thisFileUri)
type = "text/*"
getDisplayName(thisFileUri).apply { putExtra(Intent.EXTRA_TITLE, getDisplayName(
thisFileUri)) }
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)
}
@SuppressLint("Range")
fun getDisplayName(uri: Uri?): String {
// via: https://stackoverflow.com/questions/5568874/how-to-extract-the-file-name-from-uri-returned-from-intent-action-get-content
var result: String? = null;
if (uri == null) return "hauntED.md"
if (uri!!.getScheme().equals("content")) {
val cursor = getContentResolver().query(uri!!, null, null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
} finally {
cursor?.close();
}
}
if (result == null) {
result = uri!!.getPath();
val cut = result!!.lastIndexOf('/');
if (cut != -1) {
result = result.substring(cut + 1);
}
}
return result;
}
fun selectFileForSaveAs() {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/*"
putExtra(Intent.EXTRA_TITLE, getDisplayName(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(thisFileUri)}\n${getDisplayName(uri)}")
if (getDisplayName(thisFileUri) + " (1)" != getDisplayName(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) 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();") {
mdeValue =
metaData.toString() + URLDecoder.decode(it.removeSurrounding("\""))
if (mdeValue.length.toLong() == textFile.statSize) {
Log.d(javaClass.simpleName, "No change on disk, file not saved.")
return@evaluateJavascript
}
contentResolver.openFileDescriptor(thisFileUri!!, "wt")?.use {
FileOutputStream(it.fileDescriptor).use {
it.write(mdeValue.toByteArray())
}
}
Log.d(javaClass.simpleName, "File saved: ${thisFileUri}")
textFile.close()
}
})
} 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()
Log.i(javaClass.simpleName, "file newly written")
webView.evaluateJavascript("onRead();", ValueCallback<String>() {})
textFile.close()
saveMetaToSharedPrefs()
} catch (e: Exception) {
Toast.makeText(
this,
"Error during writing.\n$e",
Toast.LENGTH_LONG
).show()
}
}
@Throws(IOException::class)
private fun readFile(uri: Uri) {
//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 {
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()
}
}
mdeValue = metaData.extractMetadataFromMarkdown(i)
}
}
}
catch (e: java.io.FileNotFoundException) {
Log.d(javaClass.simpleName, "File not found during reading:\n${e.stackTraceToString()}")
this@MainActivity.runOnUiThread({
selectFileForSaveAs()
Toast.makeText(
this@MainActivity,
"File not found during reading.\n$e",
Toast.LENGTH_LONG
).show()
})
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()
})
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.codemirror.focus();" +
"easyMDE.codemirror.doc.setCursor(JSON.parse(`${metaData.cursor}`));" +
"pasteText();}"
Log.d(javaClass.simpleName, "executing in webview:\n${script}")
webView.evaluateJavascript(script, {
thisFileUri = uri
Log.d(javaClass.simpleName,"File read: ${thisFileUri}")
})
})
}
}
}
private fun saveMetaToSharedPrefs() {
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") {
selectFileForSaveAs()
} 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()
/*outState.putString("test", "onSaveInstanceState-String")*/
saveFile()
webView.evaluateJavascript("easyMDE.codemirror.doc.getCursor();") {
metaData.cursor=it
Log.i(javaClass.simpleName,"Cursor: $it")
saveMetaToSharedPrefs()
}
Log.i(javaClass.simpleName, "\"onPause\" durchlaufen")
}
override protected fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
/* findViewById<TextView>(R.id.edittext_lifecycle).text = savedInstanceState.getString("test")*/
/*webView.evaluateJavascript("getValue();", ValueCallback<String>() {
saveFile(it)
})*/
Log.i(javaClass.simpleName, "\"onRestoreInstanceState\" durchlaufen")
}
}