Skip to content

Commit

Permalink
Run each file/project with its own Rust library process
Browse files Browse the repository at this point in the history
Also improves the robustness of service
  • Loading branch information
soupslurpr committed Jan 24, 2024
1 parent dc25a63 commit f0f7888
Show file tree
Hide file tree
Showing 14 changed files with 1,092 additions and 1,064 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ android {

defaultConfig {
applicationId = "dev.soupslurpr.beautyxt"
minSdk = 23
minSdk = 29
targetSdk = 34
versionCode = 50
versionName = "0.20.0"
Expand Down
8 changes: 6 additions & 2 deletions app/src/main/kotlin/dev/soupslurpr/beautyxt/BeauTyXT.kt
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ fun BeauTyXTApp(

val openFileLauncher = rememberLauncherForActivityResult(contract = OpenDocument()) {
if (it != null) {
fileViewModel.bindIsolatedService(it)
fileViewModel.setReadOnly(false)
fileViewModel.setUri(it, context)
navController.navigate(BeauTyXTScreens.FileEdit.name)
Expand All @@ -527,6 +528,7 @@ fun BeauTyXTApp(

val createTxtFileLauncher = rememberLauncherForActivityResult(contract = CreateDocument(mimeTypePlainText)) {
if (it != null) {
fileViewModel.bindIsolatedService(it)
fileViewModel.setReadOnly(false)
fileViewModel.setUri(it, context)
navController.navigate(BeauTyXTScreens.FileEdit.name)
Expand All @@ -535,6 +537,7 @@ fun BeauTyXTApp(

val createMdFileLauncher = rememberLauncherForActivityResult(contract = CreateDocument(mimeTypeMarkdown)) {
if (it != null) {
fileViewModel.bindIsolatedService(it)
fileViewModel.setReadOnly(false)
fileViewModel.setUri(it, context)
navController.navigate(BeauTyXTScreens.FileEdit.name)
Expand All @@ -546,7 +549,7 @@ fun BeauTyXTApp(
.OpenDocumentTree()
) { projectFolderUri ->
if (projectFolderUri != null) {
typstProjectViewModel.openProject(projectFolderUri, context)
typstProjectViewModel.bindService(projectFolderUri)
navController.navigate(BeauTyXTScreens.TypstProject.name)
}
}
Expand All @@ -556,7 +559,7 @@ fun BeauTyXTApp(
.OpenDocumentTree()
) { projectFolderUri ->
if (projectFolderUri != null) {
typstProjectViewModel.openProject(projectFolderUri, context)
typstProjectViewModel.bindService(projectFolderUri)
navController.navigate(BeauTyXTScreens.TypstProject.name)
}
}
Expand Down Expand Up @@ -1355,6 +1358,7 @@ ${
},
fileViewModel = fileViewModel,
preferencesUiState = preferencesUiState,
typstProjectViewModel = typstProjectViewModel,
)
}
composable(
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/kotlin/dev/soupslurpr/beautyxt/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ class MainActivity : ComponentActivity() {

fileViewModel.setReadOnly(readOnly)

intent.data?.let { fileViewModel.setUri(it, LocalContext.current) }
intent.data?.let {
fileViewModel.bindIsolatedService(it)
fileViewModel.setUri(it, LocalContext.current)
}
}

val preferencesUiState by preferencesViewModel.uiState.collectAsState()
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/kotlin/dev/soupslurpr/beautyxt/Utils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.soupslurpr.beautyxt

import java.security.MessageDigest

fun returnHashSha256(byteArray: ByteArray): String {
return MessageDigest
.getInstance("SHA-256")
.digest(byteArray)
.joinToString("") {
"%02x".format(it)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import android.net.Uri
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import dev.soupslurpr.beautyxt.bindings.TypstCustomSourceDiagnostic
import dev.soupslurpr.beautyxt.beautyxt_rs_typst_bindings.TypstCustomSourceDiagnostic

data class TypstProjectUiState(
/** Project folder uri */
Expand Down
12 changes: 9 additions & 3 deletions app/src/main/kotlin/dev/soupslurpr/beautyxt/ui/FileEditScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import dev.soupslurpr.beautyxt.R
import dev.soupslurpr.beautyxt.constants.mimeTypeMarkdown
import dev.soupslurpr.beautyxt.data.FileUiState
import dev.soupslurpr.beautyxt.settings.PreferencesUiState

Expand Down Expand Up @@ -62,8 +63,13 @@ fun FileEditScreen(
* and then turning it on again. Or else it only updates after a character gets typed.
* Its also for updating the html when previewing using the fullscreen markdown preview
* experimental feature while having render markdown at the bottom half of the screen off.*/
if (preferencesUiState.renderMarkdown.second.value or previewMarkdownRenderedToHtmlFullscreen) {
fileViewModel.setMarkdownToHtml()
if (
(preferencesUiState.renderMarkdown.second.value or previewMarkdownRenderedToHtmlFullscreen)
&& mimeType == mimeTypeMarkdown
) {
if (fileViewModel.rustService != null) {
fileViewModel.setMarkdownToHtml()
}
}
}

Expand Down Expand Up @@ -194,7 +200,7 @@ fun FileEditScreen(
* The default text color is set to the current colorScheme's onBackground color
* to match the TextField's text color.
*/
fileViewModel.setMarkdownToHtml()
// fileViewModel.setMarkdownToHtml()
val html = """
<!DOCTYPE html>
<html>
Expand Down
84 changes: 38 additions & 46 deletions app/src/main/kotlin/dev/soupslurpr/beautyxt/ui/FileViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,30 @@ import android.content.Intent
import android.content.ServiceConnection
import android.database.Cursor
import android.net.Uri
import android.os.DeadObjectException
import android.os.IBinder
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.viewModelScope
import dev.soupslurpr.beautyxt.IFileViewModelRustLibraryAidlInterface
import dev.soupslurpr.beautyxt.constants.mimeTypeMarkdown
import dev.soupslurpr.beautyxt.data.FileUiState
import kotlinx.coroutines.Dispatchers
import dev.soupslurpr.beautyxt.returnHashSha256
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.FileOutputStream
import java.io.InputStreamReader
import kotlin.coroutines.resume

private const val TAG = "FileViewModel"

class FileViewModel(application: Application) : AndroidViewModel(application) {

Expand All @@ -41,54 +40,43 @@ class FileViewModel(application: Application) : AndroidViewModel(application) {
private val _uiState = MutableStateFlow(FileUiState())
val uiState: StateFlow<FileUiState> = _uiState.asStateFlow()

private var rustService: MutableLiveData<IFileViewModelRustLibraryAidlInterface?> = MutableLiveData(null)
var rustService: IFileViewModelRustLibraryAidlInterface? = null

private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val rustService = IFileViewModelRustLibraryAidlInterface.Stub.asInterface(service)

this@FileViewModel.rustService.postValue(rustService)
this@FileViewModel.rustService = rustService

if (uiState.value.mimeType.value == mimeTypeMarkdown) {
_uiState.value.contentConvertedToHtml.value = rustService.markdownToHtml(uiState.value.content.value)
}
}

override fun onServiceDisconnected(name: ComponentName?) {
rustService.postValue(null)
rustService = null
}
}

private val intentService = Intent(getApplication(), FileViewModelRustLibraryIsolatedService::class.java)

private suspend fun <T> LiveData<T>.awaitFirstNonNull(): T {
return withContext(Dispatchers.Main.immediate) {
suspendCancellableCoroutine { continuation ->
val observer = object : Observer<T> {
override fun onChanged(value: T) {
if (value != null) {
continuation.resume(value)
this@awaitFirstNonNull.removeObserver(this)
}
}
}

observeForever(observer)

// Handle coroutine cancellation
continuation.invokeOnCancellation {
this@awaitFirstNonNull.removeObserver(observer)
}
}
}
}

private fun bindService() {
getApplication<Application>().bindService(intentService, serviceConnection, Context.BIND_AUTO_CREATE)
}

private fun unbindService() {
getApplication<Application>().unbindService(serviceConnection)
fun bindIsolatedService(uri: Uri) {
getApplication<Application>().bindIsolatedService(
intentService,
Context.BIND_AUTO_CREATE,
returnHashSha256(uri.toString().toByteArray()),
ContextCompat.getMainExecutor(getApplication<Application>().applicationContext),
serviceConnection
)
}

init {
bindService()
private fun stopAndUnbindService() {
getApplication<Application>().stopService(intentService)
try {
getApplication<Application>().unbindService(serviceConnection)
} catch (e: IllegalArgumentException) {
Log.w(TAG, "Failed to unbind service: $e")
}
}

/**
Expand Down Expand Up @@ -210,9 +198,13 @@ class FileViewModel(application: Application) : AndroidViewModel(application) {

fun setMarkdownToHtml() {
viewModelScope.launch {
_uiState.value.contentConvertedToHtml.value = rustService.awaitFirstNonNull()!!.markdownToHtml(
uiState.value.content.value
)
try {
_uiState.value.contentConvertedToHtml.value = rustService!!.markdownToHtml(
uiState.value.content.value
)
} catch (e: DeadObjectException) {
Log.w(TAG, "setMarkdownToHtml() failed: $e")
}
}
}

Expand Down Expand Up @@ -241,6 +233,7 @@ class FileViewModel(application: Application) : AndroidViewModel(application) {
/** Set uiState to default values */
fun clearUiState() {
_uiState.value = FileUiState()
stopAndUnbindService()
}

fun exportAsHtml(uri: Uri, context: Context) {
Expand Down Expand Up @@ -303,11 +296,11 @@ class FileViewModel(application: Application) : AndroidViewModel(application) {
viewModelScope.launch {
val docx = when (uiState.value.mimeType.value) {
mimeTypeMarkdown -> {
rustService.awaitFirstNonNull()!!.markdownToDocx(uiState.value.content.value)
rustService!!.markdownToDocx(uiState.value.content.value)
}

else -> {
rustService.awaitFirstNonNull()!!.plainTextToDocx(uiState.value.content.value)
rustService!!.plainTextToDocx(uiState.value.content.value)
}
}

Expand All @@ -328,6 +321,5 @@ class FileViewModel(application: Application) : AndroidViewModel(application) {
override fun onCleared() {
super.onCleared()
clearUiState()
unbindService()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,28 @@ import dev.soupslurpr.beautyxt.IFileViewModelRustLibraryAidlInterface

class FileViewModelRustLibraryIsolatedService : Service() {
private val binder = object : IFileViewModelRustLibraryAidlInterface.Stub() {
override fun markdownToHtml(markdown: String?): String {
return dev.soupslurpr.beautyxt.bindings.markdownToHtml(markdown!!)
override fun markdownToHtml(markdown: String?): String? {
return markdown?.let {
dev.soupslurpr.beautyxt.beautyxt_rs_plain_text_and_markdown_bindings.markdownToHtml(
it
)
}
}

override fun markdownToDocx(markdown: String?): ByteArray {
return dev.soupslurpr.beautyxt.bindings.markdownToDocx(markdown!!)
override fun markdownToDocx(markdown: String?): ByteArray? {
return markdown?.let {
dev.soupslurpr.beautyxt.beautyxt_rs_plain_text_and_markdown_bindings.markdownToDocx(
it
)
}
}

override fun plainTextToDocx(plainText: String?): ByteArray {
return dev.soupslurpr.beautyxt.bindings.plainTextToDocx(plainText!!)
override fun plainTextToDocx(plainText: String?): ByteArray? {
return plainText?.let {
dev.soupslurpr.beautyxt.beautyxt_rs_plain_text_and_markdown_bindings.plainTextToDocx(
it
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,19 @@ fun StartupScreen(
onSettingsButtonClicked: () -> Unit,
fileViewModel: FileViewModel,
preferencesUiState: PreferencesUiState,
typstProjectViewModel: TypstProjectViewModel,
) {
var isOpenFileTypeAlertDialogShown by remember { mutableStateOf(false) }
var isCreateFileTypeAlertDialogShown by remember { mutableStateOf(false) }

// clear FileUiState when exiting file editor and going back to startup screen.
LaunchedEffect(key1 = Unit) {
fileViewModel.clearUiState()
if (fileViewModel.rustService != null) {
fileViewModel.clearUiState()
}
if (typstProjectViewModel.rustService != null) {
typstProjectViewModel.clearUiState()
}
}

Column(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import coil.compose.AsyncImage
import coil.decode.SvgDecoder
import coil.request.ImageRequest
import dev.soupslurpr.beautyxt.R
import dev.soupslurpr.beautyxt.bindings.TypstCustomSourceDiagnostic
import dev.soupslurpr.beautyxt.beautyxt_rs_typst_bindings.TypstCustomSourceDiagnostic
import dev.soupslurpr.beautyxt.settings.PreferencesUiState
import kotlinx.coroutines.Dispatchers

Expand Down Expand Up @@ -90,7 +90,7 @@ fun TypstProjectScreen(

typstProjectViewModel.updateProjectFileWithNewText(it, currentOpenedPath)

typstProjectViewModel.renderProjectToSvgs()
typstProjectViewModel.renderProjectToSvgs(typstProjectViewModel.rustService!!)
}

Column(
Expand Down Expand Up @@ -151,7 +151,7 @@ fun TypstProjectScreen(

typstProjectViewModel.updateProjectFileWithNewText(it, currentOpenedPath)

typstProjectViewModel.renderProjectToSvgs()
typstProjectViewModel.renderProjectToSvgs(typstProjectViewModel.rustService!!)
}

Column(
Expand Down

0 comments on commit f0f7888

Please sign in to comment.